Bug 1090925 - Add keyboard support to marionette action chains ("keyDown" and "keyUp").;r=automatedtester
authorChris Manchester <cmanchester@mozilla.com>
Tue, 10 Feb 2015 13:55:33 -0800
changeset 255628 cf6f18eea5e1f0c6e16f2fd8b7967a7934fa1b5a
parent 255627 935c00a15cedf1e832693117d7d54b48b3d5b98e
child 255629 f543e83304e91c4ac273ab9f07aa4d8df0ef6a9e
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1090925
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 1090925 - Add keyboard support to marionette action chains ("keyDown" and "keyUp").;r=automatedtester
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/tests/unit/test_key_actions.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/marionette-listener.js
testing/marionette/marionette-sendkeys.js
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -398,16 +398,35 @@ class Actions(object):
 
         '''
         element = element.id
         self.action_chain.append(['press', element, x, y])
         self.action_chain.append(['wait', time_in_seconds])
         self.action_chain.append(['release'])
         return self
 
+    def key_down(self, key_code):
+        """
+        Perform a "keyDown" action for the given key code. Modifier keys are
+        respected by the server for the course of an action chain.
+
+        :param key_code: The key to press as a result of this action.
+        """
+        self.action_chain.append(['keyDown', key_code])
+        return self
+
+    def key_up(self, key_code):
+        """
+        Perform a "keyUp" action for the given key code. Modifier keys are
+        respected by the server for the course of an action chain.
+        :param key_up: The key to release as a result of this action.
+        """
+        self.action_chain.append(['keyUp', key_code])
+        return self
+
     def perform(self):
         '''
         Sends the action chain built so far to the server side for execution and clears the current chain of actions.
         '''
         self.current_id = self.marionette._send_message('actionChain', 'value', chain=self.action_chain, nextId=self.current_id)
         self.action_chain = []
         return self
 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_key_actions.py
@@ -0,0 +1,87 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_test import MarionetteTestCase, skip_if_b2g
+from keys import Keys
+from marionette import Actions
+
+class TestKeyActions(MarionetteTestCase):
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        if self.marionette.session_capabilities['platformName'] == 'DARWIN':
+            self.mod_key = Keys.META
+        else:
+            self.mod_key = Keys.CONTROL
+        test_html = self.marionette.absolute_url("javascriptPage.html")
+        self.marionette.navigate(test_html)
+        self.reporter_element = self.marionette.find_element("id", "keyReporter")
+        self.reporter_element.click()
+        self.key_action = Actions(self.marionette)
+
+    @property
+    def key_reporter_value(self):
+        return self.reporter_element.get_attribute('value')
+
+    def test_key_action_basic_input(self):
+        self.key_action.key_down('a').key_down('b').key_down('c').perform()
+        self.assertEqual(self.key_reporter_value, "abc")
+
+    def test_upcase_input(self):
+        (self.key_action.key_down(Keys.SHIFT)
+                        .key_down('a')
+                        .key_up(Keys.SHIFT)
+                        .key_down('b')
+                        .key_down('c')
+                        .perform())
+        self.assertEqual(self.key_reporter_value, "Abc")
+
+    def test_replace_input(self):
+        self.key_action.key_down('a').key_down('b').key_down('c').perform()
+        self.assertEqual(self.key_reporter_value, "abc")
+        (self.key_action.key_down(self.mod_key)
+                        .key_down('a')
+                        .key_up(self.mod_key)
+                        .key_down('x')
+                        .perform())
+        self.assertEqual(self.key_reporter_value, "x")
+
+    def test_clear_input(self):
+        self.key_action.key_down('a').key_down('b').key_down('c').perform()
+        self.assertEqual(self.key_reporter_value, "abc")
+        (self.key_action.key_down(self.mod_key)
+                        .key_down('a')
+                        .key_down('x')
+                        .perform())
+        self.assertEqual(self.key_reporter_value, "")
+        self.key_action.key_down('a').key_down('b').key_down('c').perform()
+        self.assertEqual(self.key_reporter_value, "abc")
+
+    def test_input_with_wait(self):
+        self.key_action.key_down('a').key_down('b').key_down('c').perform()
+        (self.key_action.key_down(self.mod_key)
+                        .key_down('a')
+                        .wait(.5)
+                        .key_down('x')
+                        .perform())
+        self.assertEqual(self.key_reporter_value, "")
+
+    @skip_if_b2g
+    def test_open_in_new_window_shortcut(self):
+        el = self.marionette.find_element('id', 'updatediv')
+        start_win = self.marionette.current_chrome_window_handle
+        (self.key_action.key_down(Keys.SHIFT)
+                        .press(el)
+                        .release()
+                        .key_up(Keys.SHIFT)
+                        .perform())
+        self.wait_for_condition(
+            lambda mn: len(self.marionette.chrome_window_handles) == 2)
+        chrome_window_handles = self.marionette.chrome_window_handles
+        chrome_window_handles.remove(start_win)
+        [new_win] = chrome_window_handles
+        self.marionette.switch_to_window(new_win)
+        self.marionette.close()
+        self.marionette.switch_to_window(start_win)
+        self.assertEqual(self.key_reporter_value, "")
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -134,8 +134,9 @@ browser = false
 [test_execute_isolate.py]
 [test_click_scrolling.py]
 [test_profile_management.py]
 b2g = false
 [test_set_window_size.py]
 b2g = false
 skip-if = os == "linux" # Bug 1085717
 [test_with_using_context.py]
+[test_key_actions.py]
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -748,41 +748,52 @@ function emitTouchEvent(type, touch) {
 }
 
 /**
  * This function emit mouse event
  *   @param: doc is the current document
  *           type is the type of event to dispatch
  *           clickCount is the number of clicks, button notes the mouse button
  *           elClientX and elClientY are the coordinates of the mouse relative to the viewport
+ *           modifiers is an object of modifier keys present
  */
-function emitMouseEvent(doc, type, elClientX, elClientY, clickCount, button) {
+function emitMouseEvent(doc, type, elClientX, elClientY, button, clickCount, modifiers) {
   if (!wasInterrupted()) {
-    let loggingInfo = "emitting Mouse event of type " + type + " at coordinates (" + elClientX + ", " + elClientY + ") relative to the viewport";
+    let loggingInfo = "emitting Mouse event of type " + type +
+      " at coordinates (" + elClientX + ", " + elClientY +
+      ") relative to the viewport";
     dumpLog(loggingInfo);
     /*
     Disabled per bug 888303
     marionetteLogObj.log(loggingInfo, "TRACE");
     sendSyncMessage("Marionette:shareData",
                     {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
     marionetteLogObj.clearLogs();
     */
     let win = doc.defaultView;
-    let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
-    domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1, 0, false, 0, inputSource);
+    let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
+    let mods;
+    if (typeof modifiers != "undefined") {
+      mods = utils._parseModifiers(modifiers);
+    } else {
+      mods = 0;
+    }
+    domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1,
+                            mods, false, 0, inputSource);
   }
 }
 
 /**
  * Helper function that perform a mouse tap
  */
-function mousetap(doc, x, y) {
-  emitMouseEvent(doc, 'mousemove', x, y);
-  emitMouseEvent(doc, 'mousedown', x, y);
-  emitMouseEvent(doc, 'mouseup', x, y);
+function mousetap(doc, x, y, keyModifiers) {
+  emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
+  emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
+  emitMouseEvent(doc, 'mouseup', x, y, null, null, keyModifiers);
 }
 
 
 /**
  * This function generates a pair of coordinates relative to the viewport given a
  * target element and coordinates relative to that element's top-left corner.
  * @param 'x', and 'y' are the relative to the target.
  *        If they are not specified, then the center of the target is used.
@@ -848,17 +859,17 @@ function checkVisible(el, x, y) {
     else {
       return false;
     }
   }
   return true;
 }
 
 //x and y are coordinates relative to the viewport
-function generateEvents(type, x, y, touchId, target) {
+function generateEvents(type, x, y, touchId, target, keyModifiers) {
   lastCoordinates = [x, y];
   let doc = curFrame.document;
   switch (type) {
     case 'tap':
       if (mouseEventsOnly) {
         mousetap(target.ownerDocument, x, y);
       }
       else {
@@ -868,58 +879,60 @@ function generateEvents(type, x, y, touc
         emitTouchEvent('touchend', touch);
         mousetap(target.ownerDocument, x, y);
       }
       lastCoordinates = null;
       break;
     case 'press':
       isTap = true;
       if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mousemove', x, y);
-        emitMouseEvent(doc, 'mousedown', x, y);
+        emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
+        emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers);
       }
       else {
         let touchId = nextTouchId++;
         let touch = createATouch(target, x, y, touchId);
         emitTouchEvent('touchstart', touch);
         touchIds[touchId] = touch;
         return touchId;
       }
       break;
     case 'release':
       if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1]);
+        emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1],
+                       null, null, keyModifiers);
       }
       else {
         let touch = touchIds[touchId];
         touch = createATouch(touch.target, lastCoordinates[0], lastCoordinates[1], touchId);
         emitTouchEvent('touchend', touch);
         if (isTap) {
-          mousetap(touch.target.ownerDocument, touch.clientX, touch.clientY);
+          mousetap(touch.target.ownerDocument, touch.clientX, touch.clientY, keyModifiers);
         }
         delete touchIds[touchId];
       }
       isTap = false;
       lastCoordinates = null;
       break;
     case 'cancel':
       isTap = false;
       if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1]);
+        emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1],
+                       null, null, keyModifiers);
       }
       else {
         emitTouchEvent('touchcancel', touchIds[touchId]);
         delete touchIds[touchId];
       }
       lastCoordinates = null;
       break;
     case 'move':
       isTap = false;
       if (mouseEventsOnly) {
-        emitMouseEvent(doc, 'mousemove', x, y);
+        emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers);
       }
       else {
         touch = createATouch(touchIds[touchId].target, x, y, touchId);
         touchIds[touchId] = touch;
         emitTouchEvent('touchmove', touch);
       }
       break;
     case 'contextmenu':
@@ -1070,95 +1083,119 @@ function createATouch(el, corx, cory, to
     getCoordinateInfo(el, corx, cory);
   let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY);
   return atouch;
 }
 
 /**
  * Function to emit touch events for each finger. e.g. finger=[['press', id], ['wait', 5], ['release']]
  * touchId represents the finger id, i keeps track of the current action of the chain
+ * keyModifiers is an object keeping track keyDown/keyUp pairs through an action chain.
  */
-function actions(chain, touchId, command_id, i) {
+function actions(chain, touchId, command_id, i, keyModifiers) {
   if (typeof i === "undefined") {
     i = 0;
   }
+  if (typeof keyModifiers === "undefined") {
+    keyModifiers = {
+      shiftKey: false,
+      ctrlKey: false,
+      altKey: false,
+      metaKey: false
+    };
+  }
   if (i == chain.length) {
     sendResponse({value: touchId}, command_id);
     return;
   }
   let pack = chain[i];
   let command = pack[0];
   let el;
   let c;
   i++;
-  if (command != 'press' && command != 'wait') {
+  if (['press', 'wait', 'keyDown', 'keyUp'].indexOf(command) == -1) {
     //if mouseEventsOnly, then touchIds isn't used
     if (!(touchId in touchIds) && !mouseEventsOnly) {
       sendError("Element has not been pressed", 500, null, command_id);
       return;
     }
   }
   switch(command) {
+    case 'keyDown':
+      utils.sendKeyDown(pack[1], keyModifiers, curFrame);
+      actions(chain, touchId, command_id, i, keyModifiers);
+      break;
+    case 'keyUp':
+      utils.sendKeyUp(pack[1], keyModifiers, curFrame);
+      actions(chain, touchId, command_id, i, keyModifiers);
+      break;
     case 'press':
       if (lastCoordinates) {
-        generateEvents('cancel', lastCoordinates[0], lastCoordinates[1], touchId);
+        generateEvents('cancel', lastCoordinates[0], lastCoordinates[1],
+                       touchId, null, keyModifiers);
         sendError("Invalid Command: press cannot follow an active touch event", 500, null, command_id);
         return;
       }
       // look ahead to check if we're scrolling. Needed for APZ touch dispatching.
       if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) {
         scrolling = true;
       }
       el = elementManager.getKnownElement(pack[1], curFrame);
       c = coordinates(el, pack[2], pack[3]);
-      touchId = generateEvents('press', c.x, c.y, null, el);
-      actions(chain, touchId, command_id, i);
+      touchId = generateEvents('press', c.x, c.y, null, el, keyModifiers);
+      actions(chain, touchId, command_id, i, keyModifiers);
       break;
     case 'release':
-      generateEvents('release', lastCoordinates[0], lastCoordinates[1], touchId);
-      actions(chain, null, command_id, i);
+      generateEvents('release', lastCoordinates[0], lastCoordinates[1],
+                     touchId, null, keyModifiers);
+      actions(chain, null, command_id, i, keyModifiers);
       scrolling =  false;
       break;
     case 'move':
       el = elementManager.getKnownElement(pack[1], curFrame);
       c = coordinates(el);
-      generateEvents('move', c.x, c.y, touchId);
-      actions(chain, touchId, command_id, i);
+      generateEvents('move', c.x, c.y, touchId, null, keyModifiers);
+      actions(chain, touchId, command_id, i, keyModifiers);
       break;
     case 'moveByOffset':
-      generateEvents('move', lastCoordinates[0] + pack[1], lastCoordinates[1] + pack[2], touchId);
-      actions(chain, touchId, command_id, i);
+      generateEvents('move', lastCoordinates[0] + pack[1], lastCoordinates[1] + pack[2],
+                     touchId, null, keyModifiers);
+      actions(chain, touchId, command_id, i, keyModifiers);
       break;
     case 'wait':
       if (pack[1] != null ) {
         let time = pack[1]*1000;
         // standard waiting time to fire contextmenu
         let standard = 750;
         try {
           standard = Services.prefs.getIntPref("ui.click_hold_context_menus.delay");
         }
         catch (e){}
         if (time >= standard && isTap) {
             chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]);
             time = standard;
         }
-        checkTimer.initWithCallback(function(){actions(chain, touchId, command_id, i);}, time, Ci.nsITimer.TYPE_ONE_SHOT);
+        checkTimer.initWithCallback(function() {
+          actions(chain, touchId, command_id, i, keyModifiers);
+        }, time, Ci.nsITimer.TYPE_ONE_SHOT);
       }
       else {
-        actions(chain, touchId, command_id, i);
+        actions(chain, touchId, command_id, i, keyModifiers);
       }
       break;
     case 'cancel':
-      generateEvents('cancel', lastCoordinates[0], lastCoordinates[1], touchId);
-      actions(chain, touchId, command_id, i);
+      generateEvents('cancel', lastCoordinates[0], lastCoordinates[1],
+                     touchId, null, keyModifiers);
+      actions(chain, touchId, command_id, i, keyModifiers);
       scrolling = false;
       break;
     case 'longPress':
-      generateEvents('contextmenu', lastCoordinates[0], lastCoordinates[1], touchId);
-      actions(chain, touchId, command_id, i);
+      generateEvents('contextmenu', lastCoordinates[0], lastCoordinates[1],
+                     touchId, null, keyModifiers);
+      actions(chain, touchId, command_id, i, keyModifiers);
       break;
   }
 }
 
 /**
  * Function to start action chain on one finger
  */
 function actionChain(msg) {
--- a/testing/marionette/marionette-sendkeys.js
+++ b/testing/marionette/marionette-sendkeys.js
@@ -19,198 +19,127 @@
 let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                .getService(Ci.mozIJSSubScriptLoader);
 
 let utils = {};
 loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
 loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils);
 
+let keyModifierNames = {
+    "VK_SHIFT": 'shiftKey',
+    "VK_CONTROL": 'ctrlKey',
+    "VK_ALT": 'altKey',
+    "VK_META": 'metaKey'
+};
+
+let keyCodes = {
+  '\uE001': "VK_CANCEL",
+  '\uE002': "VK_HELP",
+  '\uE003': "VK_BACK_SPACE",
+  '\uE004': "VK_TAB",
+  '\uE005': "VK_CLEAR",
+  '\uE006': "VK_RETURN",
+  '\uE007': "VK_RETURN",
+  '\uE008': "VK_SHIFT",
+  '\uE009': "VK_CONTROL",
+  '\uE00A': "VK_ALT",
+  '\uE03D': "VK_META",
+  '\uE00B': "VK_PAUSE",
+  '\uE00C': "VK_ESCAPE",
+  '\uE00D': "VK_SPACE",  // printable
+  '\uE00E': "VK_PAGE_UP",
+  '\uE00F': "VK_PAGE_DOWN",
+  '\uE010': "VK_END",
+  '\uE011': "VK_HOME",
+  '\uE012': "VK_LEFT",
+  '\uE013': "VK_UP",
+  '\uE014': "VK_RIGHT",
+  '\uE015': "VK_DOWN",
+  '\uE016': "VK_INSERT",
+  '\uE017': "VK_DELETE",
+  '\uE018': "VK_SEMICOLON",
+  '\uE019': "VK_EQUALS",
+  '\uE01A': "VK_NUMPAD0",
+  '\uE01B': "VK_NUMPAD1",
+  '\uE01C': "VK_NUMPAD2",
+  '\uE01D': "VK_NUMPAD3",
+  '\uE01E': "VK_NUMPAD4",
+  '\uE01F': "VK_NUMPAD5",
+  '\uE020': "VK_NUMPAD6",
+  '\uE021': "VK_NUMPAD7",
+  '\uE022': "VK_NUMPAD8",
+  '\uE023': "VK_NUMPAD9",
+  '\uE024': "VK_MULTIPLY",
+  '\uE025': "VK_ADD",
+  '\uE026': "VK_SEPARATOR",
+  '\uE027': "VK_SUBTRACT",
+  '\uE028': "VK_DECIMAL",
+  '\uE029': "VK_DIVIDE",
+  '\uE031': "VK_F1",
+  '\uE032': "VK_F2",
+  '\uE033': "VK_F3",
+  '\uE034': "VK_F4",
+  '\uE035': "VK_F5",
+  '\uE036': "VK_F6",
+  '\uE037': "VK_F7",
+  '\uE038': "VK_F8",
+  '\uE039': "VK_F9",
+  '\uE03A': "VK_F10",
+  '\uE03B': "VK_F11",
+  '\uE03C': "VK_F12"
+};
+
+function getKeyCode (c) {
+  if (c in keyCodes) {
+    return keyCodes[c];
+  }
+  return c;
+};
+
+function sendKeyDown (keyToSend, modifiers, document) {
+  modifiers.type = "keydown";
+  sendSingleKey(keyToSend, modifiers, document);
+  if (["VK_SHIFT", "VK_CONTROL",
+       "VK_ALT", "VK_META"].indexOf(getKeyCode(keyToSend)) == -1) {
+    modifiers.type = "keypress";
+    sendSingleKey(keyToSend, modifiers, document);
+  }
+  delete modifiers.type;
+}
+
+function sendKeyUp (keyToSend, modifiers, document) {
+  modifiers.type = "keyup";
+  sendSingleKey(keyToSend, modifiers, document);
+  delete modifiers.type;
+}
+
+function sendSingleKey (keyToSend, modifiers, document) {
+  let keyCode = getKeyCode(keyToSend);
+  if (keyCode in keyModifierNames) {
+    let modName = keyModifierNames[keyCode];
+    modifiers[modName] = !modifiers[modName];
+  } else if (modifiers.shiftKey) {
+    keyCode = keyCode.toUpperCase();
+  }
+  utils.synthesizeKey(keyCode, modifiers, document);
+}
+
 function sendKeysToElement (document, element, keysToSend, successCallback, errorCallback, command_id, context) {
   if (context == "chrome" || checkVisible(element)) {
     element.focus();
-    var value = keysToSend.join("");
-    let hasShift = null;
-    let hasCtrl = null;
-    let hasAlt = null;
-    let hasMeta = null;
+    let modifiers = {
+      shiftKey: false,
+      ctrlKey: false,
+      altKey: false,
+      metaKey: false
+    };
+    let value = keysToSend.join("");
     for (var i = 0; i < value.length; i++) {
-      var keyCode = null;
       var c = value.charAt(i);
-      switch (c) {
-        case '\uE001':
-          keyCode = "VK_CANCEL";
-          break;
-        case '\uE002':
-          keyCode = "VK_HELP";
-          break;
-        case '\uE003':
-          keyCode = "VK_BACK_SPACE";
-          break;
-        case '\uE004':
-          keyCode = "VK_TAB";
-          break;
-        case '\uE005':
-          keyCode = "VK_CLEAR";
-          break;
-        case '\uE006':
-        case '\uE007':
-          keyCode = "VK_RETURN";
-          break;
-        case '\uE008':
-          keyCode = "VK_SHIFT";
-          hasShift = !hasShift;
-          break;
-        case '\uE009':
-          keyCode = "VK_CONTROL";
-          hasCtrl = !hasCtrl;
-          break;
-        case '\uE00A':
-          keyCode = "VK_ALT";
-          hasAlt = !hasAlt;
-          break;
-        case '\uE03D':
-          keyCode = "VK_META";
-          hasMeta = !hasMeta;
-          break;
-        case '\uE00B':
-          keyCode = "VK_PAUSE";
-          break;
-        case '\uE00C':
-          keyCode = "VK_ESCAPE";
-          break;
-        case '\uE00D':
-          keyCode = "VK_SPACE";  // printable
-          break;
-        case '\uE00E':
-          keyCode = "VK_PAGE_UP";
-          break;
-        case '\uE00F':
-          keyCode = "VK_PAGE_DOWN";
-          break;
-        case '\uE010':
-          keyCode = "VK_END";
-          break;
-        case '\uE011':
-          keyCode = "VK_HOME";
-          break;
-        case '\uE012':
-          keyCode = "VK_LEFT";
-          break;
-        case '\uE013':
-          keyCode = "VK_UP";
-          break;
-        case '\uE014':
-          keyCode = "VK_RIGHT";
-          break;
-        case '\uE015':
-          keyCode = "VK_DOWN";
-          break;
-        case '\uE016':
-          keyCode = "VK_INSERT";
-          break;
-        case '\uE017':
-          keyCode = "VK_DELETE";
-          break;
-        case '\uE018':
-          keyCode = "VK_SEMICOLON";
-          break;
-        case '\uE019':
-          keyCode = "VK_EQUALS";
-          break;
-        case '\uE01A':
-          keyCode = "VK_NUMPAD0";
-          break;
-        case '\uE01B':
-          keyCode = "VK_NUMPAD1";
-          break;
-        case '\uE01C':
-          keyCode = "VK_NUMPAD2";
-          break;
-        case '\uE01D':
-          keyCode = "VK_NUMPAD3";
-          break;
-        case '\uE01E':
-          keyCode = "VK_NUMPAD4";
-          break;
-        case '\uE01F':
-          keyCode = "VK_NUMPAD5";
-          break;
-        case '\uE020':
-          keyCode = "VK_NUMPAD6";
-          break;
-        case '\uE021':
-          keyCode = "VK_NUMPAD7";
-          break;
-        case '\uE022':
-          keyCode = "VK_NUMPAD8";
-          break;
-        case '\uE023':
-          keyCode = "VK_NUMPAD9";
-          break;
-        case '\uE024':
-          keyCode = "VK_MULTIPLY";
-          break;
-        case '\uE025':
-          keyCode = "VK_ADD";
-          break;
-        case '\uE026':
-          keyCode = "VK_SEPARATOR";
-          break;
-        case '\uE027':
-          keyCode = "VK_SUBTRACT";
-          break;
-        case '\uE028':
-          keyCode = "VK_DECIMAL";
-          break;
-        case '\uE029':
-          keyCode = "VK_DIVIDE";
-          break;
-        case '\uE031':
-          keyCode = "VK_F1";
-          break;
-        case '\uE032':
-          keyCode = "VK_F2";
-          break;
-        case '\uE033':
-          keyCode = "VK_F3";
-          break;
-        case '\uE034':
-          keyCode = "VK_F4";
-          break;
-        case '\uE035':
-          keyCode = "VK_F5";
-          break;
-        case '\uE036':
-          keyCode = "VK_F6";
-          break;
-        case '\uE037':
-          keyCode = "VK_F7";
-          break;
-        case '\uE038':
-          keyCode = "VK_F8";
-          break;
-        case '\uE039':
-          keyCode = "VK_F9";
-          break;
-        case '\uE03A':
-          keyCode = "VK_F10";
-          break;
-        case '\uE03B':
-          keyCode = "VK_F11";
-          break;
-        case '\uE03C':
-          keyCode = "VK_F12";
-          break;
-      }
-      let charCode = c.charCodeAt(0);
-      let isUpper = charCode >= 65 && charCode <= 90;
-      hasShift = isUpper || hasShift;
-      utils.synthesizeKey(keyCode || value[i],
-                          { shiftKey: hasShift, ctrlKey: hasCtrl, altKey: hasAlt, metaKey: hasMeta },
-                          document);
+      sendSingleKey(c, modifiers, document);
     }
     successCallback(command_id);
   }
   else {
     errorCallback("Element is not visible", 11, null, command_id);
   }
 };