Bug 1421323 - Ctrl+click should synthesize a contextmenu event; r?ato draft
authorMaja Frydrychowicz <mjzffr@gmail.com>
Thu, 14 Jun 2018 18:42:33 -0400
changeset 807724 4fef86985caab1cf4b6d82676c58b0244e122031
parent 807395 91db0c695f0272f00bf92c81c471a85101056d96
push id113186
push userbmo:mjzffr@gmail.com
push dateFri, 15 Jun 2018 15:12:11 +0000
reviewersato
bugs1421323
milestone62.0a1
Bug 1421323 - Ctrl+click should synthesize a contextmenu event; r?ato Except on Windows. MozReview-Commit-ID: LZ3CMyrVsDu
testing/marionette/action.js
testing/marionette/event.js
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/webdriver/tests/actions/control_click.py
testing/web-platform/tests/webdriver/tests/actions/modifier_click.py
--- a/testing/marionette/action.js
+++ b/testing/marionette/action.js
@@ -1,16 +1,18 @@
 /* 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/. */
 
 /* eslint no-dupe-keys:off */
 
 "use strict";
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
 ChromeUtils.import("chrome://marionette/content/assert.js");
 const {element} = ChromeUtils.import("chrome://marionette/content/element.js", {});
 const {
   InvalidArgumentError,
   MoveTargetOutOfBoundsError,
   UnsupportedOperationError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 ChromeUtils.import("chrome://marionette/content/event.js");
@@ -1211,26 +1213,32 @@ function dispatchPointerDown(a, inputSta
     // Append a copy of |a| with pointerUp subtype
     let copy = Object.assign({}, a, {subtype: action.PointerUp});
     action.inputsToCancel.push(copy);
 
     switch (inputState.subtype) {
       case action.PointerType.Mouse:
         let mouseEvent = new action.Mouse("mousedown", a.button);
         mouseEvent.update(inputState);
-        if (event.DoubleClickTracker.isClicked()) {
+        if (mouseEvent.ctrlKey) {
+          if (Services.appinfo.OS !== "WINNT") {
+            mouseEvent.button = 2;
+            event.DoubleClickTracker.resetClick();
+          }
+        } else if (event.DoubleClickTracker.isClicked()) {
           mouseEvent = Object.assign({},
               mouseEvent, {clickCount: 2});
         }
         event.synthesizeMouseAtPoint(
             inputState.x,
             inputState.y,
             mouseEvent,
             window);
-        if (event.MouseButton.isSecondary(a.button)) {
+        if (event.MouseButton.isSecondary(a.button) ||
+            mouseEvent.ctrlKey && Services.appinfo.OS !== "WINNT") {
           let contextMenuEvent = Object.assign({},
               mouseEvent, {type: "contextmenu"});
           event.synthesizeMouseAtPoint(
               inputState.x,
               inputState.y,
               contextMenuEvent,
               window);
         }
--- a/testing/marionette/event.js
+++ b/testing/marionette/event.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /** Provides functionality for creating and sending DOM events. */
 this.event = {};
 
 "use strict";
 /* global content, is */
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
 ChromeUtils.import("chrome://marionette/content/element.js");
 
 const dblclickTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 
 //  Max interval between two clicks that should result in a dblclick (in ms)
 const DBLCLICK_INTERVAL = 640;
 
 this.EXPORTED_SYMBOLS = ["event"];
@@ -207,17 +209,17 @@ event.parseModifiers_ = function(modifie
   }
   if (modifiers.altKey) {
     mval |= Ci.nsIDOMWindowUtils.MODIFIER_ALT;
   }
   if (modifiers.metaKey) {
     mval |= Ci.nsIDOMWindowUtils.MODIFIER_META;
   }
   if (modifiers.accelKey) {
-    if (navigator.platform.includes("Mac")) {
+    if (Services.appinfo.OS === "Darwin") {
       mval |= Ci.nsIDOMWindowUtils.MODIFIER_META;
     } else {
       mval |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL;
     }
   }
   return mval;
 };
 
@@ -645,17 +647,17 @@ function emulateToActivateModifiers_(TIP
       {key: "Alt",        attr: "altKey"},
       {key: "AltGraph",   attr: "altGraphKey"},
       {key: "Control",    attr: "ctrlKey"},
       {key: "Fn",         attr: "fnKey"},
       {key: "Meta",       attr: "metaKey"},
       {key: "OS",         attr: "osKey"},
       {key: "Shift",      attr: "shiftKey"},
       {key: "Symbol",     attr: "symbolKey"},
-      {key: isMac_(win) ? "Meta" : "Control", attr: "accelKey"},
+      {key: Services.appinfo.OS === "Darwin" ? "Meta" : "Control", attr: "accelKey"},
     ],
     lockable: [
       {key: "CapsLock",   attr: "capsLockKey"},
       {key: "FnLock",     attr: "fnLockKey"},
       {key: "NumLock",    attr: "numLockKey"},
       {key: "ScrollLock", attr: "scrollLockKey"},
       {key: "SymbolLock", attr: "symbolLockKey"},
     ],
@@ -715,25 +717,16 @@ function emulateToInactivateModifiers_(T
     let event = new KeyboardEvent("", {key: modifiers.lockable[j].key});
     TIP.keydown(event,
         TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
     TIP.keyup(event,
         TIP.KEY_NON_PRINTABLE_KEY | TIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT);
   }
 }
 
-function isMac_(win = window) {
-  if (win) {
-    try {
-      return win.navigator.platform.indexOf("Mac") > -1;
-    } catch (ex) {}
-  }
-  return navigator.platform.indexOf("Mac") > -1;
-}
-
 /* eslint-disable */
 function guessKeyNameFromKeyCode_(aKeyCode, win = window) {
   let KeyboardEvent = getKeyboardEvent_(win);
   switch (aKeyCode) {
     case KeyboardEvent.DOM_VK_CANCEL:
       return "Cancel";
     case KeyboardEvent.DOM_VK_HELP:
       return "Help";
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -404119,16 +404119,22 @@
     ]
    ],
    "webdriver/tests/accept_alert/accept.py": [
     [
      "/webdriver/tests/accept_alert/accept.py",
      {}
     ]
    ],
+   "webdriver/tests/actions/control_click.py": [
+    [
+     "/webdriver/tests/actions/control_click.py",
+     {}
+    ]
+   ],
    "webdriver/tests/actions/key.py": [
     [
      "/webdriver/tests/actions/key.py",
      {}
     ]
    ],
    "webdriver/tests/actions/key_shortcuts.py": [
     [
@@ -548367,17 +548373,17 @@
    "7cad13aa87e8a0d95dc9e35eb7d1dec7930939fb",
    "reftest"
   ],
   "css/mediaqueries/mq-calc-005.html": [
    "75334bbcf4ef412b9976c59e1efe2177ad62b465",
    "reftest"
   ],
   "css/mediaqueries/mq-case-insensitive-001.html": [
-   "5d94915b19757b3ee5ac49fb2fd119c82317ddf3",
+   "33087ab04eea830e3fcf10820fbfd167709bd18a",
    "reftest"
   ],
   "css/mediaqueries/mq-invalid-media-type-001.html": [
    "4b11afa3270c95b0a2736f114627b6f02346805a",
    "reftest"
   ],
   "css/mediaqueries/mq-invalid-media-type-002.html": [
    "42760d383b11e870f663e11624c5de8d7dfaa1ec",
@@ -618450,26 +618456,30 @@
   "webdriver/tests/actions/__init__.py": [
    "da39a3ee5e6b4b0d3255bfef95601890afd80709",
    "support"
   ],
   "webdriver/tests/actions/conftest.py": [
    "f366a25d9d5e35a2897413a265398bc7a54f7c44",
    "support"
   ],
+  "webdriver/tests/actions/control_click.py": [
+   "341a8fbfe64f5231a91999768de0b44bba121122",
+   "wdspec"
+  ],
   "webdriver/tests/actions/key.py": [
    "0b70d98a4558427666abbe9629a6cf42d69e2597",
    "wdspec"
   ],
   "webdriver/tests/actions/key_shortcuts.py": [
    "dbe27dd0b1625169fc8cc2055f8fb49d5a4a78d2",
    "wdspec"
   ],
   "webdriver/tests/actions/modifier_click.py": [
-   "f0ed71cbb6550ef496096541531f3b4fa3f4c6e2",
+   "5cb44f87588a75a5375b552bce713e7144fd02df",
    "wdspec"
   ],
   "webdriver/tests/actions/mouse.py": [
    "fb5072172d8524788147bde310d612aae5bee1b1",
    "wdspec"
   ],
   "webdriver/tests/actions/mouse_dblclick.py": [
    "dd73b09a15b438aaf77cbccdf40678a24bbabd39",
@@ -618655,17 +618665,17 @@
    "21bae43b3a6e966b8698b7c439b29a68029adc58",
    "wdspec"
   ],
   "webdriver/tests/execute_script/json_serialize_windowproxy.py": [
    "20db10d82ed2b28a22674fcdc37cac0323d33c95",
    "wdspec"
   ],
   "webdriver/tests/execute_script/user_prompts.py": [
-   "d87cc66a9d1e0003a3973b86e541b62999e9975f",
+   "4bd4e9089185505d8add4d5ebe9806498da42999",
    "wdspec"
   ],
   "webdriver/tests/find_element/__init__.py": [
    "da39a3ee5e6b4b0d3255bfef95601890afd80709",
    "support"
   ],
   "webdriver/tests/find_element/find.py": [
    "9af0db4de0d09cbf68fa43bb40145cffc7b95635",
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/actions/control_click.py
@@ -0,0 +1,77 @@
+import pytest
+
+from tests.actions.support.refine import filter_dict, get_events
+from tests.actions.support.keys import Keys
+
+
+@pytest.mark.parametrize("modifier, prop", [
+    (Keys.CONTROL, "ctrlKey"),
+    (Keys.R_CONTROL, "ctrlKey"),
+])
+def test_control_click(session, test_actions_page, key_chain, mouse_chain, modifier, prop):
+    os = session.capabilities["platformName"]
+    key_chain \
+        .pause(0) \
+        .key_down(modifier) \
+        .pause(200) \
+        .key_up(modifier)
+    outer = session.find.css("#outer", all=False)
+    mouse_chain.click(element=outer)
+    session.actions.perform([key_chain.dict, mouse_chain.dict])
+    if os == "windows_nt":
+        expected = [
+            {"type": "mousemove"},
+            {"type": "mousedown"},
+            {"type": "mouseup"},
+            {"type": "click"},
+        ]
+    else:
+        expected = [
+            {"type": "mousemove"},
+            {"type": "mousedown"},
+            {"type": "contextmenu"},
+            {"type": "mouseup"},
+        ]
+    defaults = {
+        "altKey": False,
+        "metaKey": False,
+        "shiftKey": False,
+        "ctrlKey": False
+    }
+    for e in expected:
+        e.update(defaults)
+        if e["type"] != "mousemove":
+            e[prop] = True
+    filtered_events = [filter_dict(e, expected[0]) for e in get_events(session)]
+    assert expected == filtered_events
+
+
+def test_release_control_click(session, key_reporter, key_chain, mouse_chain):
+    # The context menu stays visible during subsequent tests so let's not
+    # display it in the first place.
+    session.execute_script("""
+        var keyReporter = document.getElementById("keys");
+        document.addEventListener("contextmenu", function(e) {
+          e.preventDefault();
+        });
+    """)
+    key_chain \
+        .pause(0) \
+        .key_down(Keys.CONTROL)
+    mouse_chain \
+        .pointer_move(0, 0, origin=key_reporter) \
+        .pointer_down()
+    session.actions.perform([key_chain.dict, mouse_chain.dict])
+    session.execute_script("""
+        var keyReporter = document.getElementById("keys");
+        keyReporter.addEventListener("mousedown", recordPointerEvent);
+        keyReporter.addEventListener("mouseup", recordPointerEvent);
+        resetEvents();
+    """)
+    session.actions.release()
+    expected = [
+        {"type": "mouseup"},
+        {"type": "keyup"},
+    ]
+    events = [filter_dict(e, expected[0]) for e in get_events(session)]
+    assert events == expected
--- a/testing/web-platform/tests/webdriver/tests/actions/modifier_click.py
+++ b/testing/web-platform/tests/webdriver/tests/actions/modifier_click.py
@@ -38,46 +38,47 @@ def test_modifier_click(session, test_ac
         if e["type"] != "mousemove":
             e[prop] = True
     filtered_events = [filter_dict(e, expected[0]) for e in get_events(session)]
     assert expected == filtered_events
 
 
 def test_many_modifiers_click(session, test_actions_page, key_chain, mouse_chain):
     outer = session.find.css("#outer", all=False)
+    dblclick_timeout = 800
     key_chain \
         .pause(0) \
-        .key_down(Keys.CONTROL) \
+        .key_down(Keys.ALT) \
         .key_down(Keys.SHIFT) \
-        .pause(0) \
-        .key_up(Keys.CONTROL) \
+        .pause(dblclick_timeout) \
+        .key_up(Keys.ALT) \
         .key_up(Keys.SHIFT)
     mouse_chain \
         .pointer_move(0, 0, origin=outer) \
         .pause(0) \
         .pointer_down() \
         .pointer_up() \
         .pause(0) \
         .pause(0) \
         .pointer_down()
     session.actions.perform([key_chain.dict, mouse_chain.dict])
     expected = [
         {"type": "mousemove"},
-        # shift and ctrl presses
+        # shift and alt pressed
         {"type": "mousedown"},
         {"type": "mouseup"},
         {"type": "click"},
         # no modifiers pressed
         {"type": "mousedown"},
     ]
     defaults = {
         "altKey": False,
         "metaKey": False,
         "shiftKey": False,
         "ctrlKey": False
     }
     for e in expected:
         e.update(defaults)
     for e in expected[1:4]:
         e["shiftKey"] = True
-        e["ctrlKey"] = True
+        e["altKey"] = True
     events = [filter_dict(e, expected[0]) for e in get_events(session)]
     assert events == expected