Bug 1137557 - Part 3: Allow content to pass a dict representing the property of the keyboard event to send. r=masayuki, sr=smaug
☠☠ backed out by aa51330c9e09 ☠ ☠
authorTim Chien <timdream@gmail.com>
Sun, 23 Aug 2015 21:19:00 -0400
changeset 259028 ce86cf91f423417b4b6ca5842bca793817b3284a
parent 259027 83af10efcd3ced1f1ffaa202aeea7de03cf096f9
child 259029 dd9b9887e889d2d41a72b0b715ac886f849cb4e1
push id29268
push userryanvm@gmail.com
push dateTue, 25 Aug 2015 00:37:23 +0000
treeherdermozilla-central@08015770c9d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki, smaug
bugs1137557
milestone43.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 1137557 - Part 3: Allow content to pass a dict representing the property of the keyboard event to send. r=masayuki, sr=smaug - Overloading MozInputContext#sendKey() so it could take a dict. - An optional trailing argument for setComposition() and endComposition() methods for these methods to take the dict. - New keydown() and keyup() methods that takes dict as the only argument.
dom/inputmethod/MozKeyboard.js
dom/inputmethod/forms.js
dom/inputmethod/mochitest/mochitest.ini
dom/inputmethod/mochitest/test_bug1137557.html
dom/webidl/InputMethod.webidl
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -568,17 +568,17 @@ MozInputContext.prototype = {
 
     // Update context first before resolving promise to avoid race condition
     if (json.selectioninfo) {
       this.updateSelectionContext(json.selectioninfo, true);
     }
 
     switch (msg.name) {
       case "Keyboard:SendKey:Result:OK":
-        resolver.resolve();
+        resolver.resolve(true);
         break;
       case "Keyboard:SendKey:Result:Error":
         resolver.reject(json.error);
         break;
       case "Keyboard:GetText:Result:OK":
         resolver.resolve(json.text);
         break;
       case "Keyboard:GetText:Result:Error":
@@ -591,17 +591,17 @@ MozInputContext.prototype = {
         break;
       case "Keyboard:SequenceError":
         // Occurs when a new element got focus, but the inputContext was
         // not invalidated yet...
         resolver.reject("InputContext has expired");
         break;
       case "Keyboard:SetComposition:Result:OK": // Fall through.
       case "Keyboard:EndComposition:Result:OK":
-        resolver.resolve();
+        resolver.resolve(true);
         break;
       default:
         dump("Could not find a handler for " + msg.name);
         resolver.reject();
         break;
     }
   },
 
@@ -733,62 +733,136 @@ MozInputContext.prototype = {
       });
     });
   },
 
   deleteSurroundingText: function ic_deleteSurrText(offset, length) {
     return this.replaceSurroundingText(null, offset, length);
   },
 
-  sendKey: function ic_sendKey(keyCode, charCode, modifiers, repeat) {
-    let self = this;
-
-    // XXX: modifiers are ignored in this API method.
+  sendKey: function ic_sendKey(dictOrKeyCode, charCode, modifiers, repeat) {
+    if (typeof dictOrKeyCode === 'number') {
+      // XXX: modifiers are ignored in this API method.
 
-    return this._sendPromise(function(resolverId) {
-      cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SendKey', {
-        contextId: self._contextId,
+      return this._sendPromise((resolverId) => {
+        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+          contextId: this._contextId,
+          requestId: resolverId,
+          method: 'sendKey',
+          keyCode: dictOrKeyCode,
+          charCode: charCode,
+          repeat: repeat
+        });
+      });
+    } else if (typeof dictOrKeyCode === 'object') {
+      return this._sendPromise((resolverId) => {
+        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+          contextId: this._contextId,
+          requestId: resolverId,
+          method: 'sendKey',
+          keyboardEventDict: this._getkeyboardEventDict(dictOrKeyCode)
+        });
+      });
+    } else {
+      // XXX: Should not reach here; implies WebIDL binding error.
+      throw new TypeError('Unknown argument passed.');
+    }
+  },
+
+  keydown: function ic_keydown(dict) {
+    return this._sendPromise((resolverId) => {
+      cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+        contextId: this._contextId,
+         requestId: resolverId,
+        method: 'keydown',
+        keyboardEventDict: this._getkeyboardEventDict(dict)
+       });
+     });
+   },
+
+  keyup: function ic_keyup(dict) {
+    return this._sendPromise((resolverId) => {
+      cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+        contextId: this._contextId,
         requestId: resolverId,
-        keyCode: keyCode,
-        charCode: charCode,
-        repeat: repeat
+        method: 'keyup',
+        keyboardEventDict: this._getkeyboardEventDict(dict)
       });
     });
   },
 
-  setComposition: function ic_setComposition(text, cursor, clauses) {
+  setComposition: function ic_setComposition(text, cursor, clauses, dict) {
     let self = this;
-    return this._sendPromise(function(resolverId) {
+    return this._sendPromise((resolverId) => {
       cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetComposition', {
         contextId: self._contextId,
         requestId: resolverId,
         text: text,
         cursor: (typeof cursor !== 'undefined') ? cursor : text.length,
-        clauses: clauses || null
+        clauses: clauses || null,
+        keyboardEventDict: this._getkeyboardEventDict(dict)
       });
     });
   },
 
-  endComposition: function ic_endComposition(text) {
+  endComposition: function ic_endComposition(text, dict) {
     let self = this;
-    return this._sendPromise(function(resolverId) {
+    return this._sendPromise((resolverId) => {
       cpmmSendAsyncMessageWithKbID(self, 'Keyboard:EndComposition', {
         contextId: self._contextId,
         requestId: resolverId,
-        text: text || ''
+        text: text || '',
+        keyboardEventDict: this._getkeyboardEventDict(dict)
       });
     });
   },
 
   _sendPromise: function(callback) {
     let self = this;
     return this._ipcHelper.createPromiseWithId(function(aResolverId) {
       if (!WindowMap.isActive(self._window)) {
         self._ipcHelper.removePromiseResolver(aResolverId);
         reject('Input method is not active.');
         return;
       }
       callback(aResolverId);
     });
+  },
+
+  // Take a MozInputMethodKeyboardEventDict dict, creates a keyboardEventDict
+  // object that can be sent to forms.js
+  _getkeyboardEventDict: function(dict) {
+    if (typeof dict !== 'object' || !dict.key) {
+      return;
+    }
+
+    var keyboardEventDict = {
+      key: dict.key,
+      code: dict.code,
+      repeat: dict.repeat,
+      flags: 0
+    };
+
+    if (dict.printable) {
+      keyboardEventDict.flags |=
+        Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
+    }
+
+    if (/^[a-zA-Z0-9]$/.test(dict.key)) {
+      // keyCode must follow the key value in this range;
+      // disregard the keyCode from content.
+      keyboardEventDict.keyCode = dict.key.toUpperCase().charCodeAt(0);
+    } else if (typeof dict.keyCode === 'number') {
+      // Allow keyCode to be specified for other key values.
+      keyboardEventDict.keyCode = dict.keyCode;
+
+      // Allow keyCode to be explicitly set to zero.
+      if (dict.keyCode === 0) {
+        keyboardEventDict.flags |=
+          Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
+      }
+    }
+
+    return keyboardEventDict;
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]);
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -6,16 +6,17 @@
 
 "use strict";
 
 dump("###################################### forms.js loaded\n");
 
 let Ci = Components.interfaces;
 let Cc = Components.classes;
 let Cu = Components.utils;
+let Cr = Components.results;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 XPCOMUtils.defineLazyServiceGetter(Services, "fm",
                                    "@mozilla.org/focus-manager;1",
                                    "nsIFocusManager");
 
 /*
@@ -647,25 +648,25 @@ let FormAssistant = {
             if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
               scrollSelectionOrElementIntoView(this.focusedElement);
             }
           }.bind(this), RESIZE_SCROLL_DELAY);
         }
         break;
 
       case "keydown":
-        if (!this.focusedElement) {
+        if (!this.focusedElement || this._editing) {
           break;
         }
 
         CompositionManager.endComposition('');
         break;
 
       case "keyup":
-        if (!this.focusedElement) {
+        if (!this.focusedElement || this._editing) {
           break;
         }
 
         CompositionManager.endComposition('');
         break;
 
       case "compositionend":
         if (!this.focusedElement) {
@@ -730,65 +731,110 @@ let FormAssistant = {
               requestId: json.requestId,
               error: "Unable to start input transaction."
             });
           }
 
           break;
         }
 
-        // The naive way to figure out if the key to dispatch is printable.
-        let printable = !!json.charCode;
+        // If we receive a keyboardEventDict from json, that means the user
+        // is calling the method with the new arguments.
+        // Otherwise, we would have to construct our own keyboardEventDict
+        // based on legacy values we have received.
+        let keyboardEventDict = json.keyboardEventDict;
+        let flags = 0;
 
-        let keyboardEventDict = {
+        if (keyboardEventDict) {
+          if ('flags' in keyboardEventDict) {
+            flags = keyboardEventDict.flags;
+          }
+        } else {
+          // The naive way to figure out if the key to dispatch is printable.
+          let printable = !!json.charCode;
+
           // For printable keys, the value should be the actual character.
           // For non-printable keys, it should be a value in the D3E spec.
           // Here we make some educated guess for it.
-          key: printable ?
-            String.fromCharCode(json.charCode) :
-            guessKeyNameFromKeyCode(win.KeyboardEvent, json.keyCode),
-          // We don't have any information to tell the virtual key the
-          // user have interacted with.
-          code: "",
-          // We violate the spec here and ask TextInputProcessor not to infer
-          // this value from value of key nor code so we could keep the original
+          let key = printable ?
+              String.fromCharCode(json.charCode) :
+              guessKeyNameFromKeyCode(win.KeyboardEvent, json.keyCode);
+
+          // keyCode from content is only respected when the key is not an
+          // an alphanumeric character. We also ask TextInputProcessor not to
+          // infer this value for non-printable keys to keep the original
           // behavior.
-          keyCode: json.keyCode,
-          // We do not have the information to infer location of the virtual key
-          // either (and we would need TextInputProcessor not to compute it).
-          location: 0,
-          // This indicates the key is triggered for repeats.
-          repeat: json.repeat
-        };
+          let keyCode = (printable && /^[a-zA-Z0-9]$/.test(key)) ?
+              key.toUpperCase().charCodeAt(0) :
+              json.keyCode;
+
+          keyboardEventDict = {
+            key: key,
+            keyCode: keyCode,
+            // We don't have any information to tell the virtual key the
+            // user have interacted with.
+            code: "",
+            // We do not have the information to infer location of the virtual key
+            // either (and we would need TextInputProcessor not to compute it).
+            location: 0,
+            // This indicates the key is triggered for repeats.
+            repeat: json.repeat
+          };
+
+          flags = tip.KEY_KEEP_KEY_LOCATION_STANDARD;
+          if (!printable) {
+            flags |= tip.KEY_NON_PRINTABLE_KEY;
+          }
+          if (!keyboardEventDict.keyCode) {
+            flags |= tip.KEY_KEEP_KEYCODE_ZERO;
+          }
+        }
 
         let keyboardEvent = new win.KeyboardEvent("", keyboardEventDict);
-        let flags = tip.KEY_KEEP_KEY_LOCATION_STANDARD;
-        if (!printable) {
-          flags |= tip.KEY_NON_PRINTABLE_KEY;
-        }
-        if (!json.keyCode) {
-          flags |= tip.KEY_KEEP_KEYCODE_ZERO;
-        }
 
-        let keydownDefaultPrevented;
+        let keydownDefaultPrevented = false;
         try {
-          let consumedFlags = tip.keydown(keyboardEvent, flags);
-          keydownDefaultPrevented =
-            !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags);
-          if (!json.repeat) {
-            tip.keyup(keyboardEvent, flags);
+          switch (json.method) {
+            case 'sendKey': {
+              let consumedFlags = tip.keydown(keyboardEvent, flags);
+              keydownDefaultPrevented =
+                !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags);
+              if (!keyboardEventDict.repeat) {
+                tip.keyup(keyboardEvent, flags);
+              }
+              break;
+            }
+            case 'keydown': {
+              let consumedFlags = tip.keydown(keyboardEvent, flags);
+              keydownDefaultPrevented =
+                !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags);
+              break;
+            }
+            case 'keyup': {
+              tip.keyup(keyboardEvent, flags);
+
+              break;
+            }
           }
-        } catch (e) {
-          dump("forms.js:" + e.toString() + "\n");
+        } catch (err) {
+          dump("forms.js:" + err.toString() + "\n");
 
           if (json.requestId) {
-            sendAsyncMessage("Forms:SendKey:Result:Error", {
-              requestId: json.requestId,
-              error: "Unable to type into destoryed input."
-            });
+            if (err instanceof Ci.nsIException &&
+                err.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+              sendAsyncMessage("Forms:SendKey:Result:Error", {
+                requestId: json.requestId,
+                error: "The values specified are illegal."
+              });
+            } else {
+              sendAsyncMessage("Forms:SendKey:Result:Error", {
+                requestId: json.requestId,
+                error: "Unable to type into destroyed input."
+              });
+            }
           }
 
           break;
         }
 
         if (json.requestId) {
           if (keydownDefaultPrevented) {
             sendAsyncMessage("Forms:SendKey:Result:Error", {
@@ -910,26 +956,26 @@ let FormAssistant = {
       case "Forms:GetContext": {
         let obj = getJSON(target, this._focusCounter);
         sendAsyncMessage("Forms:GetContext:Result:OK", obj);
         break;
       }
 
       case "Forms:SetComposition": {
         CompositionManager.setComposition(target, json.text, json.cursor,
-                                          json.clauses);
+                                          json.clauses, json.keyboardEventDict);
         sendAsyncMessage("Forms:SetComposition:Result:OK", {
           requestId: json.requestId,
           selectioninfo: this.getSelectionInfo()
         });
         break;
       }
 
       case "Forms:EndComposition": {
-        CompositionManager.endComposition(json.text);
+        CompositionManager.endComposition(json.text, json.keyboardEventDict);
         sendAsyncMessage("Forms:EndComposition:Result:OK", {
           requestId: json.requestId,
           selectioninfo: this.getSelectionInfo()
         });
         break;
       }
     }
     this._editing = false;
@@ -1439,28 +1485,29 @@ function replaceSurroundingText(element,
     editor.insertText(text);
   }
   return true;
 }
 
 let CompositionManager =  {
   _isStarted: false,
   _tip: null,
+  _KeyboardEventForWin: null,
   _clauseAttrMap: {
     'raw-input':
       Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
     'selected-raw-text':
       Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE,
     'converted-text':
       Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE,
     'selected-converted-text':
       Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE
   },
 
-  setComposition: function cm_setComposition(element, text, cursor, clauses) {
+  setComposition: function cm_setComposition(element, text, cursor, clauses, dict) {
     // Check parameters.
     if (!element) {
       return;
     }
     let len = text.length;
     if (cursor > len) {
       cursor = len;
     }
@@ -1503,38 +1550,57 @@ let CompositionManager =  {
       if (!clauseLens[i]) {
         continue;
       }
       tip.appendClauseToPendingComposition(clauseLens[i], clauseAttrs[i]);
     }
     if (cursor >= 0) {
       tip.setCaretInPendingComposition(cursor);
     }
-    this._isStarted = tip.flushPendingComposition();
+
+    if (!dict) {
+      this._isStarted = tip.flushPendingComposition();
+    } else {
+      let keyboardEvent = new win.KeyboardEvent("", dict);
+      let flags = dict.flags;
+      this._isStarted = tip.flushPendingComposition(keyboardEvent, flags);
+    }
+
     if (this._isStarted) {
       this._tip = tip;
+      this._KeyboardEventForWin = win.KeyboardEvent;
     }
   },
 
-  endComposition: function cm_endComposition(text) {
+  endComposition: function cm_endComposition(text, dict) {
     if (!this._isStarted) {
       return;
     }
     let tip = this._tip;
     if (!tip) {
       return;
     }
 
-    tip.commitCompositionWith(text ? text : "");
+    text = text || "";
+    if (!dict) {
+      tip.commitCompositionWith(text);
+    } else {
+      let keyboardEvent = new this._KeyboardEventForWin("", dict);
+      let flags = dict.flags;
+      tip.commitCompositionWith(text, keyboardEvent, flags);
+    }
+
     this._isStarted = false;
     this._tip = null;
+    this._KeyboardEventForWin = null;
   },
 
   // Composition ends due to external actions.
   onCompositionEnd: function cm_onCompositionEnd() {
     if (!this._isStarted) {
       return;
     }
 
     this._isStarted = false;
     this._tip = null;
+    this._KeyboardEventForWin = null;
   }
 };
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -21,8 +21,9 @@ support-files =
 [test_bug1059163.html]
 [test_bug1066515.html]
 [test_bug1175399.html]
 [test_sendkey_cancel.html]
 [test_sync_edit.html]
 [test_two_inputs.html]
 [test_two_selects.html]
 [test_unload.html]
+[test_bug1137557.html]
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1137557.html
@@ -0,0 +1,1674 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1137557
+-->
+<head>
+  <title>Test for new API arguments accepting D3E properties</title>
+  <script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1137557">Mozilla Bug 1137557</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+  runTest();
+});
+
+let gEventDetails = [];
+let gCurrentValue = '';
+let gTestDescription = '';
+
+let appFrameScript = function appFrameScript() {
+  let input = content.document.body.firstElementChild;
+
+  input.focus();
+
+  function sendEventDetail(evt) {
+    var eventDetail;
+
+    switch (evt.type) {
+      case 'compositionstart':
+      case 'compositionupdate':
+      case 'compositionend':
+        eventDetail = {
+          type: evt.type,
+          value: input.value,
+          data: evt.data
+        };
+        break;
+
+      case 'input':
+        eventDetail = {
+          type: evt.type,
+          value: input.value
+        };
+        break;
+
+      default: // keyboard events
+        eventDetail = {
+          type: evt.type,
+          charCode: evt.charCode,
+          keyCode: evt.keyCode,
+          key: evt.key,
+          code: evt.code,
+          location: evt.location,
+          repeat: evt.repeat,
+          value: input.value,
+          shift: evt.getModifierState('Shift'),
+          capsLock: evt.getModifierState('CapsLock'),
+          control: evt.getModifierState('Control'),
+          alt: evt.getModifierState('Alt')
+        };
+        break;
+    }
+
+    sendAsyncMessage('test:eventDetail', eventDetail);
+  }
+
+  input.addEventListener('compositionstart', sendEventDetail);
+  input.addEventListener('compositionupdate', sendEventDetail);
+  input.addEventListener('compositionend', sendEventDetail);
+  input.addEventListener('input', sendEventDetail);
+  input.addEventListener('keydown', sendEventDetail);
+  input.addEventListener('keypress', sendEventDetail);
+  input.addEventListener('keyup', sendEventDetail);
+};
+
+function waitForInputContextChange() {
+  return new Promise((resolve) => {
+    navigator.mozInputMethod.oninputcontextchange = resolve;
+  });
+}
+
+function assertEventDetail(expectedDetails, testName) {
+  is(gEventDetails.length, expectedDetails.length,
+    testName + ' expects ' + expectedDetails.map(d => d.type).join(', ') + ' events, got ' + gEventDetails.map(d => d.type).join(', '));
+
+  expectedDetails.forEach((expectedDetail, j) => {
+    for (let key in expectedDetail) {
+      is(gEventDetails[j][key], expectedDetail[key],
+        testName + ' expects ' + key + ' of ' + gEventDetails[j].type + ' to be equal to ' + expectedDetail[key]);
+    }
+  });
+}
+
+function sendKeyAndAssertResult(testdata) {
+  var dict = testdata.dict;
+  var testName = gTestDescription + 'sendKey(' + JSON.stringify(dict) + ')';
+  var promise = navigator.mozInputMethod.inputcontext.sendKey(dict);
+
+  if (testdata.expectedReject) {
+    promise = promise
+      .then(() => {
+        ok(false, testName + ' should not resolve.');
+      }, (e) => {
+        ok(true, testName + ' rejects.');
+        ok(e instanceof testdata.expectedReject, 'Reject with type.');
+      })
+
+    return promise;
+  }
+
+  promise = promise
+    .then((res) => {
+      is(res, true,
+        testName + ' should resolve to true.');
+
+      var expectedEventDetail = [];
+
+      var expectedValues = testdata.expectedValues;
+
+      expectedEventDetail.push({
+        type: 'keydown',
+        key: expectedValues.key,
+        charCode: 0,
+        code: expectedValues.code || '',
+        keyCode: expectedValues.keyCode || 0,
+        location: 0,
+        repeat: expectedValues.repeat || false,
+        value: gCurrentValue,
+        shift: false,
+        capsLock: false,
+        control: false,
+        alt: false
+      });
+
+      if (testdata.expectedKeypress) {
+        expectedEventDetail.push({
+          type: 'keypress',
+          key: expectedValues.key,
+          charCode: expectedValues.charCode,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      if (testdata.expectedInput) {
+        expectedEventDetail.push({
+          type: 'input',
+          value: gCurrentValue += testdata.expectedInput
+        });
+      }
+
+      if (!testdata.expectedRepeat) {
+        expectedEventDetail.push({
+          type: 'keyup',
+          key: expectedValues.key,
+          charCode: 0,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.keyCode || 0,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      assertEventDetail(expectedEventDetail, testName);
+      gEventDetails = [];
+    }, (e) => {
+      ok(false, testName + ' should not reject. ' + e);
+    });
+
+  return promise;
+}
+
+function runSendKeyAlphabetTests() {
+  gTestDescription = 'runSendKeyAlphabetTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test the plain alphabets
+  var codeA = 'A'.charCodeAt(0);
+  for (var i = 0; i < 26; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = codeA + i;
+    let code = 'Key' + String.fromCharCode(keyCode);
+
+    [String.fromCharCode(keyCode),
+      String.fromCharCode(keyCode).toLowerCase()]
+    .forEach((chr) => {
+      // Test plain alphabet
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: '',
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test plain alphabet with keyCode set
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            keyCode: keyCode
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: '',
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test plain alphabet with keyCode set to keyCode + 1,
+      // expects keyCode to follow key value and ignore the incorrect value.
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            keyCode: keyCode + 1
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: '',
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test plain alphabet with code set
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            code: code
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: code,
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test plain alphabet with code set to Digit1,
+      // expects keyCode to follow key value.
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            code: 'Digit1'
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: 'Digit1',
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test plain alphabet with keyCode set to DOM_VK_1,
+      // expects keyCode to follow key value.
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            keyCode: KeyboardEvent.DOM_VK_1
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: '',
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test plain alphabet with code set to Digit1
+      // and keyCode set to DOM_VK_1,
+      // expects keyCode to follow key value.
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            code: 'Digit1',
+            keyCode: KeyboardEvent.DOM_VK_1
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: 'Digit1',
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+    });
+  }
+
+  return promiseQueue;
+}
+
+function runSendKeyNumberTests() {
+  gTestDescription = 'runSendKeyNumberTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test numbers
+  var code0 = '0'.charCodeAt(0);
+  for (var i = 0; i < 10; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = code0 + i;
+    let chr = String.fromCharCode(keyCode);
+
+    // Test plain number
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain number with keyCode set
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: keyCode
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain number with keyCode set to keyCode + 1,
+    // expects keyCode to follow key value and ignore the incorrect value.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: keyCode + 1
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain number with code set
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'Digit' + chr
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'Digit' + chr,
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain upper caps alphabet with code set to KeyA,
+    // expects keyCode to follow key value.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'KeyA'
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'KeyA',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain upper caps alphabet with code set to KeyA,
+    // and keyCode set to DOM_VK_A.
+    // expects keyCode to follow key value.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'KeyA',
+          keyCode: KeyboardEvent.DOM_VK_A
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'KeyA',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+  }
+
+  return promiseQueue;
+}
+
+function runSendKeyDvorakTests() {
+  gTestDescription = 'runSendKeyDvorakTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test Dvorak layout emulation
+  var qwertyCodeForDvorakKeys = [
+    'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
+    'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG',
+    'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon',
+    'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN',
+    'KeyM', 'Comma', 'Period', 'Slash'];
+  var dvorakKeys = 'PYFGCRL' +
+    'AOEUIDHTNS' +
+    'QJKXBMWVZ';
+  for (var i = 0; i < dvorakKeys.length; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = dvorakKeys.charCodeAt(i);
+    let code = qwertyCodeForDvorakKeys[i];
+
+    [dvorakKeys.charAt(i), dvorakKeys.charAt(i).toLowerCase()]
+    .forEach((chr) => {
+      // Test alphabet with code set to Qwerty code,
+      // expects keyCode to follow key value.
+      // (This is *NOT* the expected scenario for emulating a Dvorak keyboard,
+      //  even though expected results are the same.)
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            code: code
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: code,
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test alphabet with code set to Qwerty code and keyCode set,
+      // expects keyCode to follow key/keyCode value.
+      // (This is the expected scenario for emulating a Dvorak keyboard)
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            keyCode: keyCode,
+            code: code
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: code,
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+    });
+  }
+
+  var qwertyCodeForDvorakSymbols = [
+    'Minus', 'Equal',
+    'KeyQ', 'KeyW', 'KeyE', 'BracketLeft', 'BracketRight', 'Backslash',
+    'Quote', 'KeyZ'];
+
+  var shiftDvorakSymbols = '{}\"<>?+|_:';
+  var dvorakSymbols = '[]\',./=\\-;';
+  var dvorakSymbolsKeyCodes = [
+    KeyboardEvent.DOM_VK_OPEN_BRACKET,
+    KeyboardEvent.DOM_VK_CLOSE_BRACKET,
+    KeyboardEvent.DOM_VK_QUOTE,
+    KeyboardEvent.DOM_VK_COMMA,
+    KeyboardEvent.DOM_VK_PERIOD,
+    KeyboardEvent.DOM_VK_SLASH,
+    KeyboardEvent.DOM_VK_EQUALS,
+    KeyboardEvent.DOM_VK_BACK_SLASH,
+    KeyboardEvent.DOM_VK_HYPHEN_MINUS,
+    KeyboardEvent.DOM_VK_SEMICOLON
+  ];
+
+  for (var i = 0; i < dvorakSymbols.length; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = dvorakSymbolsKeyCodes[i];
+    let code = qwertyCodeForDvorakSymbols[i];
+
+    [dvorakSymbols.charAt(i), shiftDvorakSymbols.charAt(i)]
+    .forEach((chr) => {
+      // Test symbols with code set to Qwerty code,
+      // expects keyCode to be 0.
+      // (This is *NOT* the expected scenario for emulating a Dvorak keyboard)
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            code: code
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: code,
+            keyCode: 0,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+
+      // Test alphabet with code set to Qwerty code and keyCode set,
+      // expects keyCode to follow keyCode value.
+      // (This is the expected scenario for emulating a Dvorak keyboard)
+      promiseQueue = promiseQueue.then(() => {
+        return sendKeyAndAssertResult({
+          dict: {
+            key: chr,
+            keyCode: keyCode,
+            code: code
+          },
+          expectedKeypress: true,
+          expectedInput: chr,
+          expectedValues: {
+            key: chr, code: code,
+            keyCode: keyCode,
+            charCode: chr.charCodeAt(0)
+          }
+        });
+      });
+    });
+  }
+
+  return promiseQueue;
+}
+
+function runSendKeyDigitKeySymbolsTests() {
+  gTestDescription = 'runSendKeyDigitKeySymbolsTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  var digitKeySymbols = ')!@#$%^&*(';
+  for (var i = 0; i < digitKeySymbols.length; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = KeyboardEvent['DOM_VK_' + i];
+    let chr = digitKeySymbols.charAt(i);
+    let code = 'Digit' + i;
+
+    // Test plain symbol
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '', keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with keyCode set
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: keyCode
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with code set
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: code
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: code,
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with code set to KeyA,
+    // expects keyCode to be 0.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'KeyA'
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'KeyA',
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with keyCode set to DOM_VK_A,
+    // expects keyCode to follow the keyCode set.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: KeyboardEvent.DOM_VK_A
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with code set to KeyA
+    // expects keyCode to follow the keyCode set.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'KeyA',
+          keyCode: KeyboardEvent.DOM_VK_A
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'KeyA',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+  }
+
+  return promiseQueue;
+}
+
+function runSendKeyUSKeyboardSymbolsTests() {
+  gTestDescription = 'runSendKeyUSKeyboardSymbolsTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test printable symbols on US Keyboard
+  var symbols = ' ;:=+,<-_.>/?`~[{\\|]}\'\"';
+  var symbolKeyCodes = [
+    KeyboardEvent.DOM_VK_SPACE,
+    KeyboardEvent.DOM_VK_SEMICOLON,
+    KeyboardEvent.DOM_VK_SEMICOLON,
+    KeyboardEvent.DOM_VK_EQUALS,
+    KeyboardEvent.DOM_VK_EQUALS,
+    KeyboardEvent.DOM_VK_COMMA,
+    KeyboardEvent.DOM_VK_COMMA,
+    KeyboardEvent.DOM_VK_HYPHEN_MINUS,
+    KeyboardEvent.DOM_VK_HYPHEN_MINUS,
+    KeyboardEvent.DOM_VK_PERIOD,
+    KeyboardEvent.DOM_VK_PERIOD,
+    KeyboardEvent.DOM_VK_SLASH,
+    KeyboardEvent.DOM_VK_SLASH,
+    KeyboardEvent.DOM_VK_BACK_QUOTE,
+    KeyboardEvent.DOM_VK_BACK_QUOTE,
+    KeyboardEvent.DOM_VK_OPEN_BRACKET,
+    KeyboardEvent.DOM_VK_OPEN_BRACKET,
+    KeyboardEvent.DOM_VK_BACK_SLASH,
+    KeyboardEvent.DOM_VK_BACK_SLASH,
+    KeyboardEvent.DOM_VK_CLOSE_BRACKET,
+    KeyboardEvent.DOM_VK_CLOSE_BRACKET,
+    KeyboardEvent.DOM_VK_QUOTE,
+    KeyboardEvent.DOM_VK_QUOTE
+  ];
+  var symbolCodes = [
+    'Space',
+    'Semicolon',
+    'Semicolon',
+    'Equal',
+    'Equal',
+    'Comma',
+    'Comma',
+    'Minus',
+    'Minus',
+    'Period',
+    'Period',
+    'Slash',
+    'Slash',
+    'Backquote',
+    'Backquote',
+    'BracketLeft',
+    'BracketLeft',
+    'Backslash',
+    'Backslash',
+    'BracketRight',
+    'BracketRight',
+    'Quote',
+    'Quote'
+  ];
+  for (var i = 0; i < symbols.length; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = symbolKeyCodes[i];
+    let chr = symbols.charAt(i);
+    let code = symbolCodes[i];
+
+    // Test plain symbol
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with keyCode set
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: keyCode
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with code set
+    // expects keyCode to be 0.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: code
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: code,
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with code set to KeyA,
+    // expects keyCode to be 0.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'KeyA'
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'KeyA',
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with keyCode set to DOM_VK_A,
+    // expects keyCode to follow the keyCode set.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: KeyboardEvent.DOM_VK_A
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain symbol with code set to KeyA
+    // expects keyCode to follow the keyCode set.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'KeyA',
+          keyCode: KeyboardEvent.DOM_VK_A
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'KeyA',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+  }
+
+  return promiseQueue;
+}
+
+function runSendKeyGreekLettersTests() {
+  gTestDescription = 'runSendKeyGreekLettersTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test Greek letters
+  var greekLetters =
+    '\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039a\u039b\u039c' +
+    '\u039d\u039e\u039f\u03a0\u03a1\u03a3\u03a4\u03a5\u03a6\u03a7\u03a8\u03a9' +
+    '\u03b1\u03b2\u03b3\u03b4\u03b5\u03b6\u03b7\u03b8\u03b9\u03ba\u03bb\u03bc' +
+    '\u03bd\u03be\u03bf\u03c0\u03c1\u03c3\u03c4\u03c5\u03c6\u03c7\u03c8\u03c9' +
+    '\u03c2';
+  var greekLettersLayoutMap =
+    'ABGDEZHUIKLMNJOPRSTYFXCVABGDEZHUIKLMNJOPRSTYFXCVQ';
+  for (var i = 0; i < greekLetters.length; i++) {
+    // callbacks in then() are deferred; must only reference these block-scoped
+    // variable instead of i.
+    let keyCode = greekLettersLayoutMap.charCodeAt(i);
+    let chr = greekLetters.charAt(i);
+    let code = 'Key' + greekLettersLayoutMap.charAt(i);
+
+    // Test plain alphabet
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain alphabet with keyCode set
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          keyCode: keyCode
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: '',
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain alphabet with code set,
+    // expects keyCode to be 0.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: code
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: code,
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain alphabet with code set to Digit1,
+    // expects keyCode to be 0.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'Digit1'
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'Digit1',
+          keyCode: 0,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+    // Test plain alphabet with code set to Digit1,
+    // and keyCode set to DOM_VK_A.
+    // expects keyCode to follow the keyCode set.
+    promiseQueue = promiseQueue.then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: chr,
+          code: 'Digit1',
+          keyCode: KeyboardEvent.DOM_VK_A
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: 'Digit1',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+  }
+
+  return promiseQueue;
+}
+
+function runSendKeyEnterTests() {
+  gTestDescription = 'runSendKeyEnterTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test Enter with code unset
+  promiseQueue = promiseQueue.then(() => {
+    return sendKeyAndAssertResult({
+      dict: {
+        key: 'Enter'
+      },
+      expectedKeypress: true,
+      expectedInput: '\n',
+      expectedValues: {
+        key: 'Enter', code: '',
+        keyCode: KeyboardEvent.DOM_VK_RETURN,
+        charCode: 0
+      }
+    });
+  });
+
+  // Test Enter with code set
+  promiseQueue = promiseQueue.then(() => {
+    return sendKeyAndAssertResult({
+      dict: {
+        key: 'Enter',
+        code: 'Enter'
+      },
+      expectedKeypress: true,
+      expectedInput: '\n',
+      expectedValues: {
+        key: 'Enter', code: 'Enter',
+        keyCode: KeyboardEvent.DOM_VK_RETURN,
+        charCode: 0
+      }
+    });
+  });
+
+  // Test Enter with keyCode explict set to zero
+  promiseQueue = promiseQueue.then(() => {
+    return sendKeyAndAssertResult({
+      dict: {
+        key: 'Enter',
+        keyCode: 0
+      },
+      expectedKeypress: true,
+      expectedValues: {
+        key: 'Enter', code: '',
+        keyCode: 0,
+        charCode: 0
+      }
+    });
+  });
+
+  return promiseQueue;
+}
+
+function runSendKeyRejectionTests() {
+  gTestDescription = 'runSendKeyRejectionTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  promiseQueue = promiseQueue.then(() => {
+    return sendKeyAndAssertResult({
+      dict: undefined,
+      expectedReject: TypeError
+    });
+  });
+
+  return promiseQueue;
+}
+
+function setCompositionAndAssertResult(testdata) {
+  var dict = testdata.dict;
+  var testName;
+  var promise;
+
+  if (dict) {
+    testName = gTestDescription +
+      'setComposition(' + testdata.text +
+      ', undefined, undefined, '
+      + JSON.stringify(dict) + ')';
+    promise = navigator.mozInputMethod.inputcontext
+      .setComposition(testdata.text, undefined, undefined, dict);
+  } else {
+    testName = gTestDescription +
+      'setComposition(' + testdata.text + ')';
+    promise = navigator.mozInputMethod.inputcontext
+      .setComposition(testdata.text);
+  }
+
+  if (testdata.expectedReject) {
+    promise = promise
+      .then(() => {
+        ok(false, testName + ' should not resolve.');
+      }, (e) => {
+        ok(true, testName + ' rejects.');
+        ok(e instanceof testdata.expectedReject, 'Reject with type.');
+      })
+
+    return promise;
+  }
+
+  promise = promise
+    .then((res) => {
+      is(res, true,
+        testName + ' should resolve to true.');
+
+      var expectedEventDetail = [];
+
+      var expectedValues = testdata.expectedValues;
+
+      if (testdata.expectsKeyEvents &&
+          (testdata.startsComposition ||
+           testdata.dispatchKeyboardEventDuringComposition)) {
+        expectedEventDetail.push({
+          type: 'keydown',
+          key: expectedValues.key,
+          charCode: 0,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.keyCode || 0,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      if (testdata.startsComposition) {
+        expectedEventDetail.push({
+          type: 'compositionstart',
+          data: '',
+          value: gCurrentValue
+        });
+      }
+
+      expectedEventDetail.push({
+        type: 'compositionupdate',
+        data: testdata.text,
+        value: gCurrentValue
+      });
+
+      expectedEventDetail.push({
+        type: 'input',
+        value: gCurrentValue += testdata.expectedInput
+      });
+
+      if (testdata.expectsKeyEvents &&
+          testdata.dispatchKeyboardEventDuringComposition) {
+        expectedEventDetail.push({
+          type: 'keyup',
+          key: expectedValues.key,
+          charCode: 0,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.keyCode || 0,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      assertEventDetail(expectedEventDetail, testName);
+      gEventDetails = [];
+    }, (e) => {
+      ok(false, testName + ' should not reject. ' + e);
+    });
+
+  return promise;
+}
+
+function endCompositionAndAssertResult(testdata) {
+  var dict = testdata.dict;
+  var testName;
+  var promise;
+  if (dict) {
+    testName = gTestDescription +
+      'endComposition(' + testdata.text + ', ' + JSON.stringify(dict) + ')';
+    promise = navigator.mozInputMethod.inputcontext
+      .endComposition(testdata.text, dict);
+  } else {
+    testName = gTestDescription +
+      'endComposition(' + testdata.text + ')';
+    promise = navigator.mozInputMethod.inputcontext
+      .endComposition(testdata.text);
+  }
+
+  if (testdata.expectedReject) {
+    promise = promise
+      .then(() => {
+        ok(false, testName + ' should not resolve.');
+      }, (e) => {
+        ok(true, testName + ' rejects.');
+        ok(e instanceof testdata.expectedReject, 'Reject with type.');
+      })
+
+    return promise;
+  }
+
+  promise = promise
+    .then((res) => {
+      is(res, true,
+        testName + ' should resolve to true.');
+
+      var expectedEventDetail = [];
+
+      var expectedValues = testdata.expectedValues;
+
+      if (testdata.expectsKeyEvents &&
+          testdata.dispatchKeyboardEventDuringComposition) {
+        expectedEventDetail.push({
+          type: 'keydown',
+          key: expectedValues.key,
+          charCode: 0,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.keyCode || 0,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      expectedEventDetail.push({
+        type: 'compositionend',
+        data: testdata.text,
+        value: gCurrentValue
+      });
+
+      expectedEventDetail.push({
+        type: 'input',
+        value: gCurrentValue
+      });
+
+      if (testdata.expectsKeyEvents) {
+        expectedEventDetail.push({
+          type: 'keyup',
+          key: expectedValues.key,
+          charCode: 0,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.keyCode || 0,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      assertEventDetail(expectedEventDetail, testName);
+      gEventDetails = [];
+    }, (e) => {
+      ok(false, testName + ' should not reject. ' + e);
+    });
+
+  return promise;
+}
+
+function runCompositionWithKeyEventTests() {
+  var promiseQueue = Promise.resolve();
+
+  [true, false].forEach((dispatchKeyboardEventDuringComposition) => {
+    gTestDescription = 'runCompositionWithKeyEventTests() (dispatchKeyboardEvent =' + dispatchKeyboardEventDuringComposition + '): ';
+
+    promiseQueue = promiseQueue
+      .then(() => {
+        SpecialPowers.setBoolPref(
+          'dom.keyboardevent.dispatch_during_composition',
+          dispatchKeyboardEventDuringComposition);
+      })
+      .then(() => {
+        return setCompositionAndAssertResult({
+          text: 'foo',
+          expectsKeyEvents: true,
+          startsComposition: true,
+          dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
+          expectedInput: 'foo',
+          dict: {
+            key: 'a',
+            code: 'KeyA',
+            keyCode: KeyboardEvent.DOM_VK_A
+          },
+          expectedValues: {
+            key: 'a',
+            code: 'KeyA',
+            keyCode: KeyboardEvent.DOM_VK_A
+          }
+        });
+      })
+      .then(() => {
+        return setCompositionAndAssertResult({
+          text: 'foobar',
+          expectsKeyEvents: true,
+          startsComposition: false,
+          dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
+          expectedInput: 'bar',
+          dict: {
+            key: 'a',
+            code: 'KeyA',
+            keyCode: KeyboardEvent.DOM_VK_A
+          },
+          expectedValues: {
+            key: 'a',
+            code: 'KeyA',
+            keyCode: KeyboardEvent.DOM_VK_A
+          }
+        });
+      })
+      .then(() => {
+        return endCompositionAndAssertResult({
+          text: 'foobar',
+          expectsKeyEvents: true,
+          dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
+          expectedInput: '',
+          dict: {
+            key: 'a',
+            code: 'KeyA',
+            keyCode: KeyboardEvent.DOM_VK_A
+          },
+          expectedValues: {
+            key: 'a',
+            code: 'KeyA',
+            keyCode: KeyboardEvent.DOM_VK_A
+          }
+        });
+      })
+      .then(() => {
+        SpecialPowers.clearUserPref(
+          'dom.keyboardevent.dispatch_during_composition');
+      });
+  });
+
+  return promiseQueue;
+}
+
+function runCompositionWithoutKeyEventTests() {
+  var promiseQueue = Promise.resolve();
+
+  gTestDescription = 'runCompositionWithoutKeyEventTests(): ';
+
+  promiseQueue = promiseQueue
+    .then(() => {
+      return setCompositionAndAssertResult({
+        text: 'foo',
+        expectsKeyEvents: false,
+        startsComposition: true,
+        expectedInput: 'foo'
+      });
+    })
+    .then(() => {
+      return setCompositionAndAssertResult({
+        text: 'foobar',
+        expectsKeyEvents: false,
+        startsComposition: false,
+        expectedInput: 'bar'
+      });
+    })
+    .then(() => {
+      return endCompositionAndAssertResult({
+        text: 'foobar',
+        expectsKeyEvents: false,
+        expectedInput: ''
+      });
+    });
+
+  return promiseQueue;
+}
+
+function keydownAndAssertResult(testdata) {
+  var dict = testdata.dict;
+  var testName = gTestDescription + 'keydown(' + JSON.stringify(dict) + ')';
+  var promise = navigator.mozInputMethod.inputcontext.keydown(dict);
+
+  if (testdata.expectedReject) {
+    promise = promise
+      .then(() => {
+        ok(false, testName + ' should not resolve.');
+      }, (e) => {
+        ok(true, testName + ' rejects.');
+        ok(e instanceof testdata.expectedReject, 'Reject with type.');
+      })
+
+    return promise;
+  }
+
+  promise = promise
+    .then((res) => {
+      is(res, true,
+        testName + ' should resolve to true.');
+
+      var expectedEventDetail = [];
+
+      var expectedValues = testdata.expectedValues;
+
+      expectedEventDetail.push({
+        type: 'keydown',
+        key: expectedValues.key,
+        charCode: 0,
+        code: expectedValues.code || '',
+        keyCode: expectedValues.keyCode || 0,
+        location: 0,
+        repeat: expectedValues.repeat || false,
+        value: gCurrentValue,
+        shift: false,
+        capsLock: false,
+        control: false,
+        alt: false
+      });
+
+      if (testdata.expectedKeypress) {
+        expectedEventDetail.push({
+          type: 'keypress',
+          key: expectedValues.key,
+          charCode: expectedValues.charCode,
+          code: expectedValues.code || '',
+          keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode,
+          location: 0,
+          repeat: expectedValues.repeat || false,
+          value: gCurrentValue,
+          shift: false,
+          capsLock: false,
+          control: false,
+          alt: false
+        });
+      }
+
+      if (testdata.expectedInput) {
+        expectedEventDetail.push({
+          type: 'input',
+          value: gCurrentValue += testdata.expectedInput
+        });
+      }
+
+      assertEventDetail(expectedEventDetail, testName);
+      gEventDetails = [];
+    }, (e) => {
+      ok(false, testName + ' should not reject. ' + e);
+    });
+
+  return promise;
+}
+
+function keyupAndAssertResult(testdata) {
+  var dict = testdata.dict;
+  var testName = gTestDescription + 'keyup(' + JSON.stringify(dict) + ')';
+  var promise = navigator.mozInputMethod.inputcontext.keyup(dict);
+
+  if (testdata.expectedReject) {
+    promise = promise
+      .then(() => {
+        ok(false, testName + ' should not resolve.');
+      }, (e) => {
+        ok(true, testName + ' rejects.');
+        ok(e instanceof testdata.expectedReject, 'Reject with type.');
+      })
+
+    return promise;
+  }
+
+  promise = promise
+    .then((res) => {
+      is(res, true,
+        testName + ' should resolve to true.');
+
+      var expectedEventDetail = [];
+
+      var expectedValues = testdata.expectedValues;
+
+      expectedEventDetail.push({
+        type: 'keyup',
+        key: expectedValues.key,
+        charCode: 0,
+        code: expectedValues.code || '',
+        keyCode: expectedValues.keyCode || 0,
+        location: 0,
+        repeat: expectedValues.repeat || false,
+        value: gCurrentValue,
+        shift: false,
+        capsLock: false,
+        control: false,
+        alt: false
+      });
+
+      assertEventDetail(expectedEventDetail, testName);
+      gEventDetails = [];
+    }, (e) => {
+      ok(false, testName + ' should not reject. ' + e);
+    });
+
+  return promise;
+}
+
+function runKeyDownUpTests() {
+  gTestDescription = 'runKeyDownUpTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  let chr = 'a';
+  let code = 'KeyA';
+  let keyCode = KeyboardEvent.DOM_VK_A;
+
+  promiseQueue = promiseQueue
+    .then(() => {
+      return keydownAndAssertResult({
+        dict: {
+          key: chr,
+          code: code,
+          keyCode: keyCode
+        },
+        expectedKeypress: true,
+        expectedInput: chr,
+        expectedValues: {
+          key: chr, code: code,
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    })
+    .then(() => {
+      return keyupAndAssertResult({
+        dict: {
+          key: chr,
+          code: code,
+          keyCode: keyCode
+        },
+        expectedValues: {
+          key: chr, code: code,
+          keyCode: keyCode,
+          charCode: chr.charCodeAt(0)
+        }
+      });
+    });
+
+  return promiseQueue;
+}
+
+function runKeyDownUpRejectionTests() {
+  gTestDescription = 'runKeyDownUpRejectionTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  promiseQueue = promiseQueue.then(() => {
+    return keydownAndAssertResult({
+      dict: undefined,
+      expectedReject: TypeError
+    });
+  });
+
+  promiseQueue = promiseQueue.then(() => {
+    return keyupAndAssertResult({
+      dict: undefined,
+      expectedReject: TypeError
+    });
+  });
+
+  return promiseQueue;
+}
+
+function runRepeatTests() {
+  gTestDescription = 'runRepeatTests(): ';
+  var promiseQueue = Promise.resolve();
+
+  // Test repeat
+  promiseQueue = promiseQueue
+    .then(() => {
+      return sendKeyAndAssertResult({
+        dict: {
+          key: 'A',
+          repeat: true
+        },
+        expectedKeypress: true,
+        expectedRepeat: true,
+        expectedInput: 'A',
+        expectedValues: {
+          repeat: true,
+          key: 'A', code: '',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: 'A'.charCodeAt(0)
+        }
+      });
+    })
+    .then(() => {
+      return keyupAndAssertResult({
+        dict: {
+          key: 'A'
+        },
+        expectedKeypress: true,
+        expectedRepeat: true,
+        expectedInput: 'A',
+        expectedValues: {
+          key: 'A', code: '',
+          keyCode: KeyboardEvent.DOM_VK_A,
+          charCode: 'A'.charCodeAt(0)
+        }
+      });
+    });
+
+  return promiseQueue;
+}
+
+function runTest() {
+  let im = navigator.mozInputMethod;
+
+  // Set current page as an input method.
+  SpecialPowers.wrap(im).setActive(true);
+
+  let iframe = document.createElement('iframe');
+  iframe.src = 'data:text/html,<html><body><textarea rows=30 cols=30></textarea></body></html>';
+  iframe.setAttribute('mozbrowser', true);
+  document.body.appendChild(iframe);
+
+  let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+  iframe.addEventListener('mozbrowserloadend', function() {
+    mm.addMessageListener('test:eventDetail', function(msg) {
+      gEventDetails.push(msg.data);
+    });
+    mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+  });
+
+  waitForInputContextChange()
+    .then(() => {
+      var inputcontext = navigator.mozInputMethod.inputcontext;
+
+      ok(!!inputcontext, 'Receving the first input context');
+    })
+    .then(() => runSendKeyAlphabetTests())
+    .then(() => runSendKeyNumberTests())
+    .then(() => runSendKeyDvorakTests())
+    .then(() => runSendKeyDigitKeySymbolsTests())
+    .then(() => runSendKeyUSKeyboardSymbolsTests())
+    .then(() => runSendKeyGreekLettersTests())
+    .then(() => runSendKeyEnterTests())
+    .then(() => runSendKeyRejectionTests())
+    .then(() => runCompositionWithKeyEventTests())
+    .then(() => runCompositionWithoutKeyEventTests())
+    .then(() => runKeyDownUpTests())
+    .then(() => runKeyDownUpRejectionTests())
+    .then(() => runRepeatTests())
+    .catch((err) => {
+      console.error(err);
+      is(false, err.message);
+    })
+    .then(() => {
+      var p = waitForInputContextChange();
+
+      // Revoke our right from using the IM API.
+      SpecialPowers.wrap(im).setActive(false);
+
+      return p;
+    })
+    .then(() => {
+      var inputcontext = navigator.mozInputMethod.inputcontext;
+
+      is(inputcontext, null, 'Receving null input context');
+
+      inputmethod_cleanup();
+    })
+    .catch((err) => {
+      console.error(err);
+      is(false, err.message);
+    });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
--- a/dom/webidl/InputMethod.webidl
+++ b/dom/webidl/InputMethod.webidl
@@ -187,79 +187,188 @@ interface MozInputContext: EventTarget {
      *
      * A dict is provided in the detail property of the event containing the new values, and
      * an "ownAction" property to denote the event is the result of our own mutation to
      * the input field.
      */
     attribute EventHandler onsurroundingtextchange;
 
     /*
-      * send a character with its key events.
-      * @param modifiers this paramater is no longer honored.
-      * @param repeat indicates whether a key would be sent repeatedly.
-      * @return true if succeeds. Otherwise false if the input context becomes void.
-      * Alternative: sendKey(KeyboardEvent event), but we will likely
-      * waste memory for creating the KeyboardEvent object.
-      * Note that, if you want to send a key n times repeatedly, make sure set
-      * parameter repeat to true and invoke sendKey n-1 times, and then set
-      * repeat to false in the last invoke.
-      */
-    Promise<boolean> sendKey(long keyCode, long charCode, long modifiers, optional boolean repeat);
+     * Send a string/character with its key events. There are two ways of invocating 
+     * the method for backward compability purpose.
+     *
+     * (1) The recommended way, allow specifying DOM level 3 properties like |code|.
+     * @param dictOrKeyCode See MozInputMethodKeyboardEventDict.
+     * @param charCode disregarded
+     * @param modifiers disregarded
+     * @param repeat disregarded
+     *
+     * (2) Deprecated, reserved for backward compability.
+     * @param dictOrKeyCode keyCode of the key to send, should be one of the DOM_VK_ value in KeyboardEvent.
+     * @param charCode charCode of the character, should be 0 for non-printable keys.
+     * @param modifiers this paramater is no longer honored.
+     * @param repeat indicates whether a key would be sent repeatedly.
+     *
+     * @return A promise. Resolve to true if succeeds.
+     *                    Rejects to a string indicating the error.
+     *
+     * Note that, if you want to send a key n times repeatedly, make sure set
+     * parameter repeat to true and invoke sendKey n times, and invoke keyup
+     * after the end of the input.
+     */
+    Promise<boolean> sendKey((MozInputMethodRequiredKeyboardEventDict or long) dictOrKeyCode,
+                             optional long charCode, 
+                             optional long modifiers, 
+                             optional boolean repeat);
+
+    /*
+     * Send a string/character with keydown, and keypress events.
+     * keyup should be called afterwards to ensure properly sequence.
+     *
+     * @param dict See MozInputMethodKeyboardEventDict.
+     *
+     * @return A promise. Resolve to true if succeeds.
+     *                    Rejects to a string indicating the error.
+     */
+    Promise<boolean> keydown(MozInputMethodRequiredKeyboardEventDict dict);
+
+    /*
+     * Send a keyup event. keydown should be called first to ensure properly sequence.
+     *
+     * @param dict See MozInputMethodKeyboardEventDict.
+     *
+     * @return A promise. Resolve to true if succeeds.
+     *                    Rejects to a string indicating the error.
+     *
+     */
+    Promise<boolean> keyup(MozInputMethodRequiredKeyboardEventDict dict);
 
     /*
      * Set current composing text. This method will start composition or update
      * composition if it has started. The composition will be started right
      * before the cursor position and any selected text will be replaced by the
      * composing text. When the composition is started, calling this method can
      * update the text and move cursor winthin the range of the composing text.
      * @param text The new composition text to show.
      * @param cursor The new cursor position relative to the start of the
      * composition text. The cursor should be positioned within the composition
      * text. This means the value should be >= 0 and <= the length of
      * composition text. Defaults to the lenght of composition text, i.e., the
      * cursor will be positioned after the composition text.
      * @param clauses The array of composition clause information. If not set,
      * only one clause is supported.
-     *
+     * @param dict The properties of the keyboard event that cause the composition
+     * to set. keydown or keyup event will be fired if it's necessary.
+     * For compatibility, we recommend that you should always set this argument 
+     * if it's caused by a key operation.
+     * 
      * The composing text, which is shown with underlined style to distinguish
      * from the existing text, is used to compose non-ASCII characters from
      * keystrokes, e.g. Pinyin or Hiragana. The composing text is the
      * intermediate text to help input complex character and is not committed to
      * current input field. Therefore if any text operation other than
      * composition is performed, the composition will automatically end. Same
      * apply when the inputContext is lost during an unfinished composition
      * session.
      *
      * To finish composition and commit text to current input field, an IME
      * should call |endComposition|.
      */
-    // XXXbz what is this promise resolved with?
-    Promise<any> setComposition(DOMString text, optional long cursor,
-                                optional sequence<CompositionClauseParameters> clauses);
+    Promise<boolean> setComposition(DOMString text, 
+                                    optional long cursor,
+                                    optional sequence<CompositionClauseParameters> clauses,
+                                    optional MozInputMethodKeyboardEventDict dict);
 
     /*
      * End composition, clear the composing text and commit given text to
      * current input field. The text will be committed before the cursor
      * position.
      * @param text The text to commited before cursor position. If empty string
      * is given, no text will be committed.
+     * @param dict The properties of the keyboard event that cause the composition
+     * to end. keydown or keyup event will be fired if it's necessary.
+     * For compatibility, we recommend that you should always set this argument 
+     * if it's caused by a key operation.
      *
      * Note that composition always ends automatically with nothing to commit if
      * the composition does not explicitly end by calling |endComposition|, but
      * is interrupted by |sendKey|, |setSelectionRange|,
      * |replaceSurroundingText|, |deleteSurroundingText|, user moving the
      * cursor, changing the focus, etc.
      */
-    // XXXbz what is this promise resolved with?
-    Promise<any> endComposition(optional DOMString text);
+    Promise<boolean> endComposition(optional DOMString text, 
+                                    optional MozInputMethodKeyboardEventDict dict);
 };
 
 enum CompositionClauseSelectionType {
   "raw-input",
   "selected-raw-text",
   "converted-text",
   "selected-converted-text"
 };
 
 dictionary CompositionClauseParameters {
   DOMString selectionType = "raw-input";
   long length;
 };
+
+/*
+ * A MozInputMethodKeyboardEventDictBase contains the following properties,
+ * indicating the properties of the keyboard event caused.
+ *
+ * This is the base dictionary type for us to create two child types that could
+ * be used as argument type in two types of methods, as WebIDL parser required.
+ * 
+ */
+dictionary MozInputMethodKeyboardEventDictBase {
+  /*
+   * String/character to output, or a registered name of non-printable key.
+   * (To be defined in the inheriting dictionary types.)
+   */
+  // DOMString key;
+  /*
+   * String/char indicating the virtual hardware key pressed. Optional.
+   * Must be a value defined in
+   * http://www.w3.org/TR/DOM-Level-3-Events-code/#keyboard-chording-virtual
+   * If your keyboard app emulates physical keyboard layout, this value should
+   * not be empty string. Otherwise, it should be empty string.
+   */
+  DOMString code = "";
+  /*
+   * keyCode of the keyboard event. Optional.
+   * To be disregarded if |key| is an alphanumeric character.
+   * If the key causes inputting a character and if your keyboard app emulates
+   * a physical keyboard layout, this value should be same as the value used
+   * by Firefox for desktop. If the key causes inputting an ASCII character
+   * but if your keyboard app doesn't emulate any physical keyboard layouts,
+   * the value should be proper value for the key value.
+   */
+  long? keyCode;
+  /*
+   * Indicates whether a key would be sent repeatedly. Optional.
+   */
+  boolean repeat = false;
+  /*
+   * Optional. True if |key| property is explicitly referring to a printable key.
+   * When this is set, key will be printable even if the |key| value matches any
+   * of the registered name of non-printable keys.
+   */
+  boolean printable = false;
+};
+
+/*
+ * For methods like setComposition() and endComposition(), the optional
+ * dictionary type argument is really optional when all of it's property
+ * are optional.
+ * This dictionary type is used to denote that argument.
+ */
+dictionary MozInputMethodKeyboardEventDict : MozInputMethodKeyboardEventDictBase {
+  DOMString? key;
+};
+
+/*
+ * For methods like keydown() and keyup(), the dictionary type argument is
+ * considered required only if at least one of it's property is required.
+ * This dictionary type is used to denote that argument.
+ */
+dictionary MozInputMethodRequiredKeyboardEventDict : MozInputMethodKeyboardEventDictBase {
+  required DOMString key;
+};