Bug 1436926 - part 1: EventUtils.synthesizeKey() should guess KeyboardEvent.code value if it's not specified explicitly r?smaug draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Fri, 09 Feb 2018 18:29:03 +0900
changeset 754465 5d59c460a69ab4bee0b70c8affb21080e16c8a05
parent 754399 38b3c1d03a594664c6b32c35533734283c258f43
child 754466 d9d0acc562c29aa32e849e156cfa48527bf99f2f
push id98886
push usermasayuki@d-toybox.com
push dateTue, 13 Feb 2018 16:39:17 +0000
reviewerssmaug
bugs1436926
milestone60.0a1
Bug 1436926 - part 1: EventUtils.synthesizeKey() should guess KeyboardEvent.code value if it's not specified explicitly r?smaug For testing key operation stricter, all automated tests should set KeyboardEvent.code properly. However, in most cases, automated tests assumes that active keyboard layout is US (ANSI) keyboard layout. Therefore, synthesizeKey() should guess KeyboardEvent.code value from KeyboardEvent.key value as US keyboard layout automatically. MozReview-Commit-ID: 85JyyaBwpfI
testing/mochitest/tests/SimpleTest/EventUtils.js
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -75,16 +75,40 @@ function _EU_isWin(aWindow = window) {
   if (aWindow) {
     try {
       return aWindow.navigator.platform.indexOf("Win") > -1;
     } catch (ex) {}
   }
   return navigator.platform.indexOf("Win") > -1;
 }
 
+function _EU_isLinux(aWindow = window) {
+  if (window._EU_OS) {
+    return window._EU_OS == "linux";
+  }
+  if (aWindow) {
+    try {
+      return aWindow.navigator.platform.startsWith("Linux");
+    } catch (ex) {}
+  }
+  return navigator.platform.startsWith("Linux");
+}
+
+function _EU_isAndroid(aWindow = window) {
+  if (window._EU_OS) {
+    return window._EU_OS == "android";
+  }
+  if (aWindow) {
+    try {
+      return aWindow.navigator.userAgent.includes("Android");
+    } catch (ex) {}
+  }
+  return navigator.userAgent.includes("Android");
+}
+
 function _EU_maybeWrap(o) {
   var c = Object.getOwnPropertyDescriptor(window, 'Components');
   return c.value && !c.writable ? o : SpecialPowers.wrap(o);
 }
 
 function _EU_maybeUnwrap(o) {
   var c = Object.getOwnPropertyDescriptor(window, 'Components');
   return c.value && !c.writable ? o : SpecialPowers.unwrap(o);
@@ -839,18 +863,24 @@ function _computeKeyCodeFromChar(aChar)
  * aKey should be:
  *  - key value (recommended).  If you specify a non-printable key name,
  *    append "KEY_" prefix.  Otherwise, specifying a printable key, the
  *    key value should be specified.
  *  - keyCode name starting with "VK_" (e.g., VK_RETURN).  This is available
  *    only for compatibility with legacy API.  Don't use this with new tests.
  *
  * aEvent is an object which may contain the properties:
- *  - code: If you emulates a physical keyboard's key event, this should be
- *          specified.
+ *  - code: If you don't specify this explicitly, it'll be guessed from aKey
+ *          of US keyboard layout.  Note that this value may be different
+ *          between browsers.  For example, "Insert" is never set only on
+ *          macOS since actual key operation won't cause this code value.
+ *          In such case, the value becomes empty string.
+ *          If you need to emulate non-US keyboard layout or virtual keyboard
+ *          which doesn't emulate hardware key input, you should set this value
+ *          to empty string explicitly.
  *  - repeat: If you emulates auto-repeat, you should set the count of repeat.
  *            This method will automatically synthesize keydown (and keypress).
  *  - location: If you want to specify this, you can specify this explicitly.
  *              However, if you don't specify this value, it will be computed
  *              from code value.
  *  - type: Basically, you shouldn't specify this.  Then, this function will
  *          synthesize keydown (, keypress) and keyup.
  *          If keydown is specified, this only fires keydown (and keypress if
@@ -1434,16 +1464,279 @@ function _guessKeyNameFromKeyCode(aKeyCo
       return "EraseEof";
     case KeyboardEvent.DOM_VK_PLAY:
       return "Play";
     default:
       return "Unidentified";
   }
 }
 
+function _guessCodeFromKeyName(aKeyName, aLocation, aWindow = window)
+{
+  var KeyboardEvent = _getKeyboardEvent(aWindow);
+  if (aLocation === KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) {
+    switch (aKeyName) {
+      case "Insert":
+        return _EU_isMac(aWindow) ? "" : "Numpad0";
+      case "End":
+        return _EU_isMac(aWindow) ? "" : "Numpad1";
+      case "ArrowDown":
+        return _EU_isMac(aWindow) ? "" : "Numpad2";
+      case "PageDown":
+        return _EU_isMac(aWindow) ? "" : "Numpad3";
+      case "ArrowLeft":
+        return _EU_isMac(aWindow) ? "" : "Numpad4";
+      case "Clear":
+        return !_EU_isWin(aWindow) ? "" : "Numpad5";
+      case "ArrowRight":
+        return _EU_isMac(aWindow) ? "" : "Numpad6";
+      case "Home":
+        return _EU_isMac(aWindow) ? "" : "Numpad7";
+      case "ArrowUp":
+        return _EU_isMac(aWindow) ? "" : "Numpad8";
+      case "PageUp":
+        return _EU_isMac(aWindow) ? "" : "Numpad9";
+      case "Delete":
+        return _EU_isMac(aWindow) ? "" : "NumpadDecimal";
+      case "Enter":
+        return "NumpadEnter";
+      case "=":
+        return "NumpadEqual";
+      case "+":
+        return "NumpadAdd";
+      case "-":
+        return "NumpadSubtract";
+      case "*":
+        return "NumpadMultiply";
+      case "/":
+        return "NumpadDivide";
+      case "0":
+      case "1":
+      case "2":
+      case "3":
+      case "4":
+      case "5":
+      case "6":
+      case "7":
+      case "8":
+      case "9":
+        return "Numpad" + aKeyName;
+      default:
+        // FYI: NumLock (Clear on macOS) should be DOM_KEY_LOCATION_STANDARD.
+        return "";
+    }
+  }
+
+  if (aLocation === undefined ||
+      aLocation === KeyboardEvent.DOM_KEY_LOCATION_LEFT ||
+      aLocation === KeyboardEvent.DOM_KEY_LOCATION_RIGHT) {
+    function getLeftOrRightCode(aKey)
+    {
+      if (aLocation === undefined) {
+        return aKey + "Left";
+      }
+      if (aLocation === KeyboardEvent.DOM_KEY_LOCATION_LEFT) {
+        return aKey + "Left";
+      }
+      if (aLocation === KeyboardEvent.DOM_KEY_LOCATION_RIGHT) {
+        return aKey + "Right";
+      }
+      // If location value is illegal for left or right key, perhaps,
+      // it tries to emulate a virtual keyboard's event or something odd.
+      return "";
+    }
+    switch (aKeyName) {
+      case "Alt":
+      case "Control":
+      case "Shift":
+        return getLeftOrRightCode(aKeyName);
+      case "Meta":
+        if (_EU_isWin(aWindow)) {
+          return "";
+        }
+        if (_EU_isAndroid(aWindow) || _EU_isMac(aWindow)) {
+          return getLeftOrRightCode("OS");
+        }
+        // On Linux, Alt + Shift is "Meta".
+        return getLeftOrRightCode("Alt");
+      case "OS": // bug 1232918
+        if (_EU_isAndroid(aWindow) || _EU_isMac(aWindow)) {
+          return "";
+        }
+        return getLeftOrRightCode("OS");
+    }
+  }
+
+  if (aLocation === undefined || aLocation === 0) {
+    switch (aKeyName) {
+      // Same as key name.
+      case "ArrowDown":
+      case "ArrowLeft":
+      case "ArrowRight":
+      case "ArrowUp":
+      case "Backspace":
+      case "CapsLock":
+      case "ContextMenu":
+      case "Delete":
+      case "End":
+      case "Enter":
+      case "Escape":
+      case "F1":
+      case "F2":
+      case "F3":
+      case "F4":
+      case "F5":
+      case "F6":
+      case "F7":
+      case "F8":
+      case "F9":
+      case "F10":
+      case "F11":
+      case "F12":
+      case "F13":
+      case "F14":
+      case "F15":
+      case "F16":
+      case "F17":
+      case "F18":
+      case "F19":
+      case "F20":
+      case "Home":
+      case "PageDown":
+      case "PageUp":
+      case "Tab":
+        return aKeyName;
+      // Same as key name but not available only on macOS.
+      case "BrowserBack":
+      case "BrowserFavorites":
+      case "BrowserForward":
+      case "BrowserRefresh":
+      case "BrowserSearch":
+      case "BrowserStop":
+      case "F21":
+      case "F22":
+      case "F23":
+      case "F24":
+      case "Insert":
+      case "MediaPlayPause":
+      case "MediaStop":
+      case "MediaTrackNext":
+      case "MediaTrackPrevious":
+      case "Pause":
+      case "PrintScreen":
+      case "ScrollLock":
+        return _EU_isMac(aWindow) ? "" : aKeyName;
+      // Same as key name but available only on macOS.
+      case "Clear":
+      case "Fn":
+        return _EU_isMac(aWindow) ? aKeyName : "";
+      // Same as key name but not available only on Windows.
+      case "Help":
+        return _EU_isMac(aWindow) ? "" : aKeyName;
+      // Same as key name but available only on Windows and Linux.
+      case "BrowserHome":
+        return _EU_isWin(aWindow) || _EU_isLinux(aWindow) ? aKeyName : "";
+      // Same as key name but available only on Linux and Android.
+      case "Eject":
+      case "WakeUp":
+        return _EU_isLinux(aWindow) || _EU_isAndroid(aWindow) ? aKeyName : "";
+      // Special cases.
+      case "Break":
+        return !_EU_isMac(aWindow) ? "Pause" : "";
+      case "AudioVolumeDown":
+      case "AudioVolumeMute":
+      case "AudioVolumeUp":
+        return aKeyName.substr("Audio".length); // bug 1272579
+      case "LaunchApplication1":
+        return !_EU_isMac(aWindow) ? "LaunchApp1" : "";
+      case "LaunchApplication2":
+        return _EU_isWin(aWindow) || _EU_isLinux(aWindow) ? "LaunchApp2" : "";
+      // TODO: this function and synthesizeKey() should be able to take
+      //       keyboard layout name optionally.
+      default:
+        if (aKeyName.length != 1) {
+          return "";
+        }
+        if (aKeyName.charCodeAt(0) >= "A".charCodeAt(0) &&
+            aKeyName.charCodeAt(0) <= "Z".charCodeAt(0)) {
+          return "Key" + aKeyName;
+        }
+        if (aKeyName.charCodeAt(0) >= "a".charCodeAt(0) &&
+            aKeyName.charCodeAt(0) <= "z".charCodeAt(0)) {
+          return "Key" + aKeyName.toUpperCase();
+        }
+        if (aKeyName.charCodeAt(0) >= "0".charCodeAt(0) &&
+            aKeyName.charCodeAt(0) <= "9".charCodeAt(0)) {
+          return "Digit" + aKeyName;
+        }
+        switch (aKeyName) {
+          case " ":
+            return "Space";
+          case "`":
+          case "~":
+            return "Backquote";
+          case "\\":
+          case "|":
+            return "Backslash";
+          case "[":
+          case "{":
+            return "BracketLeft";
+          case "]":
+          case "}":
+            return "BracketRight";
+          case ",":
+          case "<":
+            return "Comma";
+          case ")":
+            return "Digit0";
+          case "!":
+            return "Digit1";
+          case "@":
+            return "Digit2";
+          case "#":
+            return "Digit3";
+          case "$":
+            return "Digit4";
+          case "%":
+            return "Digit5";
+          case "^":
+            return "Digit6";
+          case "&":
+            return "Digit7";
+          case "*":
+            return "Digit8";
+          case "(":
+            return "Digit9";
+          case "=":
+          case "+":
+            return "Equal";
+          case "-":
+          case "_":
+            return "Minus";
+          case ".":
+          case ">":
+            return "Period";
+          case "'":
+          case "\"":
+            return "Quote";
+          case ";":
+          case ":":
+            return "Semicolon";
+          case "/":
+          case "?":
+            return "Slash";
+          default:
+            return "";
+        }
+    }
+  }
+
+  return "";
+}
+
 function _createKeyboardEventDictionary(aKey, aKeyEvent, aWindow = window) {
   var result = { dictionary: null, flags: 0 };
   var keyCodeIsDefined = "keyCode" in aKeyEvent;
   var keyCode =
     (keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255) ?
       aKeyEvent.keyCode : 0;
   var keyName = "Unidentified";
   if (aKey.indexOf("KEY_") == 0) {
@@ -1461,23 +1754,26 @@ function _createKeyboardEventDictionary(
     if (!keyCodeIsDefined) {
       keyCode = _computeKeyCodeFromChar(aKey.charAt(0));
     }
     if (!keyCode) {
       result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
     }
     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
   }
+  var code = "code" in aKeyEvent ?
+    aKeyEvent.code :
+    _guessCodeFromKeyName(keyName, aKeyEvent.location, aWindow);
   var locationIsDefined = "location" in aKeyEvent;
   if (locationIsDefined && aKeyEvent.location === 0) {
     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
   }
   result.dictionary = {
     key: keyName,
-    code: "code" in aKeyEvent ? aKeyEvent.code : "",
+    code: code,
     location: locationIsDefined ? aKeyEvent.location : 0,
     repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false,
     keyCode: keyCode,
   };
   return result;
 }
 
 function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window)