Bug 1119609 part.11 TextEventDispatcher shouldn't allow to begin input transaction during dispatching a event r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 19 Feb 2015 15:50:20 +0900
changeset 229852 0ea20218201750faf0c76202e58fb8c18a2dd628
parent 229851 38827922cd9bbe51471d4cbd4691fcdcfaf6d203
child 229853 bcf39cf754249c23b5204fd091ba73475ba9bd31
push id28300
push userryanvm@gmail.com
push dateThu, 19 Feb 2015 23:52:47 +0000
treeherdermozilla-central@56f090df5480 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1119609
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1119609 part.11 TextEventDispatcher shouldn't allow to begin input transaction during dispatching a event r=smaug
dom/base/TextInputProcessor.cpp
dom/base/test/chrome/window_nsITextInputProcessor.xul
dom/interfaces/base/nsITextInputProcessor.idl
widget/TextEventDispatcher.cpp
widget/TextEventDispatcher.h
--- a/dom/base/TextInputProcessor.cpp
+++ b/dom/base/TextInputProcessor.cpp
@@ -151,20 +151,20 @@ TextInputProcessor::BeginInputTransactio
     return NS_OK;
   }
 
   // If this instance is composing, don't allow to initialize again.
   if (mDispatcher && mDispatcher->IsComposing()) {
     return NS_ERROR_ALREADY_INITIALIZED;
   }
 
-  // And also if another instance is composing with the new dispatcher, it'll
-  // fail to steal its ownership.  Then, we should not throw an exception,
-  // just return false.
-  if (dispatcher->IsComposing()) {
+  // And also if another instance is composing with the new dispatcher or
+  // dispatching an event, it'll fail to steal its ownership.  Then, we should
+  // not throw an exception, just return false.
+  if (dispatcher->IsComposing() || dispatcher->IsDispatchingEvent()) {
     return NS_OK;
   }
 
   // This instance has finished preparing to link to the dispatcher.  Therefore,
   // let's forget the old dispatcher and purpose.
   UnlinkFromTextEventDispatcher();
 
   if (aForTests) {
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul
@@ -160,18 +160,462 @@ function runBeginInputTransactionMethodT
 
   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 startComposition() throws an exception after ownership is strolen.
+  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during startComposition().
+  var events = [];
+  input.addEventListener("compositionstart", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.startComposition();");
+  }, false);
+  TIP1.beginInputTransaction(window, simpleCallback);
+  TIP1.startComposition();
+  is(events.length, 1,
+     description + "compositionstart event should be fired by TIP1.startComposition()");
+  TIP1.cancelComposition();
+
+  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during flushPendingComposition().
+  events = [];
+  input.addEventListener("compositionstart", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  input.addEventListener("compositionupdate", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionupdate event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  TIP1.beginInputTransaction(window, simpleCallback);
+  TIP1.setPendingCompositionString(composingStr);
+  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
+  TIP1.flushPendingComposition();
+  is(events.length, 4,
+     description + "compositionstart, compositionupdate, text and input events should be fired by TIP1.flushPendingComposition()");
+  is(events[0].type, "compositionstart",
+     description + "events[0] should be compositionstart");
+  is(events[1].type, "compositionupdate",
+     description + "events[1] should be compositionupdate");
+  is(events[2].type, "text",
+     description + "events[2] should be text");
+  is(events[3].type, "input",
+     description + "events[3] should be input");
+  TIP1.cancelComposition();
+
+  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition().
+  events = [];
+  TIP1.beginInputTransaction(window, simpleCallback);
+  TIP1.setPendingCompositionString(composingStr);
+  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
+  TIP1.flushPendingComposition();
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.commitComposition();");
+  }, false);
+  input.addEventListener("compositionend", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionend event handler during a call of TIP1.commitComposition();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.commitComposition();");
+  }, false);
+  TIP1.commitComposition();
+  is(events.length, 3,
+     description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
+  is(events[0].type, "text",
+     description + "events[0] should be text");
+  is(events[1].type, "compositionend",
+     description + "events[1] should be compositionend");
+  is(events[2].type, "input",
+     description + "events[2] should be input");
+
+  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition("bar").
+  events = [];
+  input.addEventListener("compositionstart", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("compositionupdate", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction during compositionupdate event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction during text event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("compositionend", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction during compositionend event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction during input event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  TIP1.beginInputTransaction(window, simpleCallback);
+  TIP1.commitComposition("bar");
+  is(events.length, 5,
+     description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitComposition(\"bar\")");
+  is(events[0].type, "compositionstart",
+     description + "events[0] should be compositionstart");
+  is(events[1].type, "compositionupdate",
+     description + "events[1] should be compositionupdate");
+  is(events[2].type, "text",
+     description + "events[2] should be text");
+  is(events[3].type, "compositionend",
+     description + "events[3] should be compositionend");
+  is(events[4].type, "input",
+     description + "events[4] should be input");
+
+  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during cancelComposition().
+  events = [];
+  TIP1.beginInputTransaction(window, simpleCallback);
+  TIP1.setPendingCompositionString(composingStr);
+  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
+  TIP1.flushPendingComposition();
+  input.addEventListener("compositionupdate", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionupdate event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from text event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  input.addEventListener("compositionend", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from compositionend event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  TIP1.cancelComposition();
+  is(events.length, 4,
+     description + "compositionupdate, text, compositionend and input events should be fired by TIP1.cancelComposition()");
+  is(events[0].type, "compositionupdate",
+     description + "events[0] should be compositionupdate");
+  is(events[1].type, "text",
+     description + "events[1] should be text");
+  is(events[2].type, "compositionend",
+     description + "events[2] should be compositionend");
+  is(events[3].type, "input",
+     description + "events[3] should be input");
+
+  // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
+  events = [];
+  TIP1.beginInputTransaction(window, simpleCallback);
+  input.addEventListener("keydown", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from keydown event handler during a call of TIP1.keydown();");
+  }, false);
+  input.addEventListener("keypress", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from keypress event handler during a call of TIP1.keydown();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from input event handler during a call of TIP1.keydown();");
+  }, false);
+  input.addEventListener("keyup", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransaction(window, simpleCallback),
+       description + "TIP2 shouldn't be able to begin input transaction from keyup event handler during a call of TIP1.keyup();");
+  }, false);
+  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
+  TIP1.keydown(keyA);
+  TIP1.keyup(keyA);
+  is(events.length, 4,
+     description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
+  is(events[0].type, "keydown",
+     description + "events[0] should be keydown");
+  is(events[1].type, "keypress",
+     description + "events[1] should be keypress");
+  is(events[2].type, "input",
+     description + "events[2] should be input");
+  is(events[3].type, "keyup",
+     description + "events[3] should be keyup");
+
+  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during startComposition().
+  var events = [];
+  input.addEventListener("compositionstart", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.startComposition();");
+  }, false);
+  TIP1.beginInputTransactionForTests(window);
+  TIP1.startComposition();
+  is(events.length, 1,
+     description + "compositionstart event should be fired by TIP1.startComposition()");
+  TIP1.cancelComposition();
+
+  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during flushPendingComposition().
+  events = [];
+  input.addEventListener("compositionstart", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  input.addEventListener("compositionupdate", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionupdate event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.flushPendingComposition();");
+  }, false);
+  TIP1.beginInputTransactionForTests(window);
+  TIP1.setPendingCompositionString(composingStr);
+  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
+  TIP1.flushPendingComposition();
+  is(events.length, 4,
+     description + "compositionstart, compositionupdate, text and input events should be fired by TIP1.flushPendingComposition()");
+  is(events[0].type, "compositionstart",
+     description + "events[0] should be compositionstart");
+  is(events[1].type, "compositionupdate",
+     description + "events[1] should be compositionupdate");
+  is(events[2].type, "text",
+     description + "events[2] should be text");
+  is(events[3].type, "input",
+     description + "events[3] should be input");
+  TIP1.cancelComposition();
+
+  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition().
+  events = [];
+  TIP1.beginInputTransactionForTests(window, simpleCallback);
+  TIP1.setPendingCompositionString(composingStr);
+  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
+  TIP1.flushPendingComposition();
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.commitComposition();");
+  }, false);
+  input.addEventListener("compositionend", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionend event handler during a call of TIP1.commitComposition();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.commitComposition();");
+  }, false);
+  TIP1.commitComposition();
+  is(events.length, 3,
+     description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
+  is(events[0].type, "text",
+     description + "events[0] should be text");
+  is(events[1].type, "compositionend",
+     description + "events[1] should be compositionend");
+  is(events[2].type, "input",
+     description + "events[2] should be input");
+
+  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition("bar").
+  events = [];
+  input.addEventListener("compositionstart", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("compositionupdate", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests during compositionupdate event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests during text event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("compositionend", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests during compositionend event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests during input event handler TIP1.commitComposition(\"bar\");");
+  }, false);
+  TIP1.beginInputTransactionForTests(window);
+  TIP1.commitComposition("bar");
+  is(events.length, 5,
+     description + "compositionstart, compositionupdate, text, compositionend and input events should be fired by TIP1.commitComposition(\"bar\")");
+  is(events[0].type, "compositionstart",
+     description + "events[0] should be compositionstart");
+  is(events[1].type, "compositionupdate",
+     description + "events[1] should be compositionupdate");
+  is(events[2].type, "text",
+     description + "events[2] should be text");
+  is(events[3].type, "compositionend",
+     description + "events[3] should be compositionend");
+  is(events[4].type, "input",
+     description + "events[4] should be input");
+
+  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during cancelComposition().
+  events = [];
+  TIP1.beginInputTransactionForTests(window, simpleCallback);
+  TIP1.setPendingCompositionString(composingStr);
+  TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
+  TIP1.flushPendingComposition();
+  input.addEventListener("compositionupdate", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionupdate event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  input.addEventListener("text", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from text event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  input.addEventListener("compositionend", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from compositionend event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.cancelComposition();");
+  }, false);
+  TIP1.cancelComposition();
+  is(events.length, 4,
+     description + "compositionupdate, text, compositionend and input events should be fired by TIP1.cancelComposition()");
+  is(events[0].type, "compositionupdate",
+     description + "events[0] should be compositionupdate");
+  is(events[1].type, "text",
+     description + "events[1] should be text");
+  is(events[2].type, "compositionend",
+     description + "events[2] should be compositionend");
+  is(events[3].type, "input",
+     description + "events[3] should be input");
+
+  // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
+  events = [];
+  TIP1.beginInputTransactionForTests(window);
+  input.addEventListener("keydown", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests for tests from keydown event handler during a call of TIP1.keydown();");
+  }, false);
+  input.addEventListener("keypress", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from keypress event handler during a call of TIP1.keydown();");
+  }, false);
+  input.addEventListener("input", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from input event handler during a call of TIP1.keydown();");
+  }, false);
+  input.addEventListener("keyup", function (aEvent) {
+    events.push(aEvent);
+    input.removeEventListener(aEvent.type, arguments.callee, false);
+    ok(!TIP2.beginInputTransactionForTests(window),
+       description + "TIP2 shouldn't be able to begin input transaction for tests from keyup event handler during a call of TIP1.keyup();");
+  }, false);
+  var keyA = new KeyboardEvent("", { key: "a", code: "KeyA", keyCode: KeyboardEvent.DOM_VK_A });
+  TIP1.keydown(keyA);
+  TIP1.keyup(keyA);
+  is(events.length, 4,
+     description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
+  is(events[0].type, "keydown",
+     description + "events[0] should be keydown");
+  is(events[1].type, "keypress",
+     description + "events[1] should be keypress");
+  is(events[2].type, "input",
+     description + "events[2] should be input");
+  is(events[3].type, "keyup",
+     description + "events[3] should be keyup");
+
+  // Let's check if 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.contains("NS_ERROR_NOT_INITIALIZED"),
        description + "TIP1.startComposition() should cause throwing an exception including NS_ERROR_NOT_INITIALIZED");
--- a/dom/interfaces/base/nsITextInputProcessor.idl
+++ b/dom/interfaces/base/nsITextInputProcessor.idl
@@ -115,25 +115,16 @@ interface nsITextInputProcessorCallback;
  *   // You don't need to specify .keyCode value if it's non-printable key
  *   // because it can be computed from .key value.
  *   // If you specify non-zero value to .keyCode, it'll be used.
  *   var keyEvent = new KeyboardEvent("", { code: "Enter", key: "Enter" });
  *   if (TIP.keydown(keyEvent)) {
  *     // Handle its default action
  *   }
  *
- *   // You should call beginInputTransaction() or
- *   // beginInputTransactionForTests() before every keydow() or keyup() call
- *   // because somebody stole the right to compose on the window.  E.g., in
- *   // this case, another TIP might start composition at receiving a DOM event
- *   // which was caused by preceding keydown().
- *   if (!TIP.beginInputTransaction(window, callback)) {
- *     return; // You failed to get the rights to dispatch key events
- *   }
- *
  *   // Even if keydown event was consumed, keyup event should be dispatched.
  *   if (TIP.keyup(keyEvent)) {
  *     // Handle its default action
  *   }
  *
  * Example #9 JS-IME or JS-Keyboard should dispatch key events even during
  *            composition (printable key case):
  *
@@ -184,20 +175,16 @@ interface nsITextInputProcessorCallback;
  *   // physical keyboard.  Otherwise, use same value with physical keyboard's
  *   // same key.
  *   var keyEvent = new KeyboardEvent("", { code: "KeyA", key: "a",
  *                                          keyCode: KeyboardEvent.DOM_VK_A });
  *   if (TIP.keydown(keyEvent)) {
  *     // Handle its default action
  *   }
  *
- *   if (!TIP.beginInputTransaction(window, callback)) {
- *     return; // You failed to get the rights to dispatch key events
- *   }
- *
  *   // Even if keydown event was consumed, keyup event should be dispatched.
  *   if (TIP.keyup(keyEvent)) {
  *     // Handle its default action
  *   }
  *
  * Example #10 JS-Keyboard doesn't need to initialize modifier states at
  *             calling either keydown() or keyup().
  *
--- a/widget/TextEventDispatcher.cpp
+++ b/widget/TextEventDispatcher.cpp
@@ -19,16 +19,17 @@ namespace widget {
 /******************************************************************************
  * TextEventDispatcher
  *****************************************************************************/
 
 bool TextEventDispatcher::sDispatchKeyEventsDuringComposition = false;
 
 TextEventDispatcher::TextEventDispatcher(nsIWidget* aWidget)
   : mWidget(aWidget)
+  , mDispatchingEvent(0)
   , mForTests(false)
   , mIsComposing(false)
 {
   MOZ_RELEASE_ASSERT(mWidget, "aWidget must not be nullptr");
 
   static bool sInitialized = false;
   if (!sInitialized) {
     Preferences::AddBoolVarCache(
@@ -61,18 +62,19 @@ TextEventDispatcher::BeginInputTransacti
   if (NS_WARN_IF(!aListener)) {
     return NS_ERROR_INVALID_ARG;
   }
   nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
   if (listener) {
     if (listener == aListener && mForTests == aForTests) {
       return NS_OK;
     }
-    // If this has composition, any other listener can steal ownership.
-    if (IsComposing()) {
+    // If this has composition or is dispatching an event, any other listener
+    // can steal ownership.
+    if (IsComposing() || mDispatchingEvent) {
       return NS_ERROR_ALREADY_INITIALIZED;
     }
   }
   mListener = do_GetWeakReference(aListener);
   mForTests = aForTests;
   if (listener && listener != aListener) {
     listener->OnRemovedFrom(this);
   }
@@ -108,35 +110,47 @@ void
 TextEventDispatcher::InitEvent(WidgetGUIEvent& aEvent) const
 {
   aEvent.time = PR_IntervalNow();
   aEvent.refPoint = LayoutDeviceIntPoint(0, 0);
   aEvent.mFlags.mIsSynthesizedForTests = mForTests;
 }
 
 nsresult
+TextEventDispatcher::DispatchEvent(nsIWidget* aWidget,
+                                   WidgetGUIEvent& aEvent,
+                                   nsEventStatus& aStatus)
+{
+  nsRefPtr<TextEventDispatcher> kungFuDeathGrip(this);
+  nsCOMPtr<nsIWidget> widget(aWidget);
+  mDispatchingEvent++;
+  nsresult rv = widget->DispatchEvent(&aEvent, aStatus);
+  mDispatchingEvent--;
+  return rv;
+}
+
+nsresult
 TextEventDispatcher::StartComposition(nsEventStatus& aStatus)
 {
   aStatus = nsEventStatus_eIgnore;
 
   nsresult rv = GetState();
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (NS_WARN_IF(mIsComposing)) {
     return NS_ERROR_FAILURE;
   }
 
   mIsComposing = true;
-  nsCOMPtr<nsIWidget> widget(mWidget);
   WidgetCompositionEvent compositionStartEvent(true, NS_COMPOSITION_START,
-                                               widget);
+                                               mWidget);
   InitEvent(compositionStartEvent);
-  rv = widget->DispatchEvent(&compositionStartEvent, aStatus);
+  rv = DispatchEvent(mWidget, compositionStartEvent, aStatus);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
@@ -207,17 +221,17 @@ TextEventDispatcher::CommitComposition(n
 
   uint32_t message = aCommitString ? NS_COMPOSITION_COMMIT :
                                      NS_COMPOSITION_COMMIT_AS_IS;
   WidgetCompositionEvent compositionCommitEvent(true, message, widget);
   InitEvent(compositionCommitEvent);
   if (message == NS_COMPOSITION_COMMIT) {
     compositionCommitEvent.mData = *aCommitString;
   }
-  rv = widget->DispatchEvent(&compositionCommitEvent, aStatus);
+  rv = DispatchEvent(widget, compositionCommitEvent, aStatus);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
@@ -273,19 +287,17 @@ TextEventDispatcher::DispatchKeyboardEve
     if (!sDispatchKeyEventsDuringComposition || aMessage == NS_KEY_PRESS) {
       return false;
     }
     // XXX If there was mOnlyContentDispatch for this case, it might be useful
     //     because our chrome doesn't assume that key events are fired during
     //     composition.
   }
 
-  nsCOMPtr<nsIWidget> widget(mWidget);
-
-  WidgetKeyboardEvent keyEvent(true, aMessage, widget);
+  WidgetKeyboardEvent keyEvent(true, aMessage, mWidget);
   InitEvent(keyEvent);
   keyEvent.AssignKeyEventData(aKeyboardEvent, false);
 
   if (aStatus == nsEventStatus_eConsumeNoDefault) {
     // If the key event should be dispatched as consumed event, marking it here.
     // This is useful to prevent double action.  E.g., when the key was already
     // handled by system, our chrome shouldn't handle it.
     keyEvent.mFlags.mDefaultPrevented = true;
@@ -326,17 +338,17 @@ TextEventDispatcher::DispatchKeyboardEve
   // XXX Currently, we don't support to dispatch key event with native key
   //     event information.
   keyEvent.mNativeKeyEvent = nullptr;
   // XXX Currently, we don't support to dispatch key events with data for
   // plugins.
   keyEvent.mPluginEvent.Clear();
   // TODO: Manage mUniqueId here.
 
-  widget->DispatchEvent(&keyEvent, aStatus);
+  DispatchEvent(mWidget, keyEvent, aStatus);
   return true;
 }
 
 bool
 TextEventDispatcher::MaybeDispatchKeypressEvents(
                        const WidgetKeyboardEvent& aKeyboardEvent,
                        nsEventStatus& aStatus)
 {
@@ -469,16 +481,17 @@ TextEventDispatcher::PendingComposition:
       NS_WARNING("Caret position is out of the composition string");
       Clear();
       return NS_ERROR_ILLEGAL_VALUE;
     }
     EnsureClauseArray();
     mClauses->AppendElement(mCaret);
   }
 
+  nsRefPtr<TextEventDispatcher> kungFuDeathGrip(aDispatcher);
   nsCOMPtr<nsIWidget> widget(aDispatcher->mWidget);
   WidgetCompositionEvent compChangeEvent(true, NS_COMPOSITION_CHANGE, widget);
   aDispatcher->InitEvent(compChangeEvent);
   compChangeEvent.mData = mString;
   if (mClauses) {
     MOZ_ASSERT(!mClauses->IsEmpty(),
                "mClauses must be non-empty array when it's not nullptr");
     compChangeEvent.mRanges = mClauses;
@@ -491,17 +504,17 @@ TextEventDispatcher::PendingComposition:
 
   rv = aDispatcher->StartCompositionAutomaticallyIfNecessary(aStatus);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
   if (aStatus == nsEventStatus_eConsumeNoDefault) {
     return NS_OK;
   }
-  rv = widget->DispatchEvent(&compChangeEvent, aStatus);
+  rv = aDispatcher->DispatchEvent(widget, compChangeEvent, aStatus);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
 } // namespace widget
--- a/widget/TextEventDispatcher.h
+++ b/widget/TextEventDispatcher.h
@@ -78,16 +78,21 @@ public:
 
   /**
    * IsComposing() returns true after calling StartComposition() and before
    * calling CommitComposition().
    */
   bool IsComposing() const { return mIsComposing; }
 
   /**
+   * IsDispatchingEvent() returns true while this instance dispatching an event.
+   */
+  bool IsDispatchingEvent() const { return mDispatchingEvent > 0; }
+
+  /**
    * StartComposition() starts composition explicitly.
    */
   nsresult StartComposition(nsEventStatus& aStatus);
 
   /**
    * CommitComposition() commits composition.
    *
    * @param aCommitString   If this is null, commits with the last composition
@@ -226,16 +231,19 @@ private:
     nsAutoString mString;
     nsRefPtr<TextRangeArray> mClauses;
     TextRange mCaret;
 
     void EnsureClauseArray();
   };
   PendingComposition mPendingComposition;
 
+  // While dispatching an event, this is incremented.
+  uint16_t mDispatchingEvent;
+
   bool mForTests;
   // See IsComposing().
   bool mIsComposing;
 
   // If this is true, keydown and keyup events are dispatched even when there
   // is a composition.
   static bool sDispatchKeyEventsDuringComposition;
 
@@ -245,16 +253,23 @@ private:
 
   /**
    * InitEvent() initializes aEvent.  This must be called before dispatching
    * the event.
    */
   void InitEvent(WidgetGUIEvent& aEvent) const;
 
   /**
+   * DispatchEvent() dispatches aEvent on aWidget.
+   */
+  nsresult DispatchEvent(nsIWidget* aWidget,
+                         WidgetGUIEvent& aEvent,
+                         nsEventStatus& aStatus);
+
+  /**
    * StartCompositionAutomaticallyIfNecessary() starts composition if it hasn't
    * been started it yet.
    *
    * @param aStatus         If it succeeded to start composition normally, this
    *                        returns nsEventStatus_eIgnore.  Otherwise, e.g.,
    *                        the composition is canceled during dispatching
    *                        compositionstart event, this returns
    *                        nsEventStatus_eConsumeNoDefault.  In this case,