Bug 1365886 - [marionette] Allow performActions to operate on chrome elements r=marionette-reviewers,jdescottes,whimboo
authorMaja Frydrychowicz <mjzffr@gmail.com>
Fri, 23 Oct 2020 15:32:08 +0000
changeset 554267 3a4b62af959713ca7229195de50b281b17a57a2e
parent 554266 2871269eaab6cfdfc919f9b40760eb6c90d05bd4
child 554268 1980f87855fc557e7ef6bb24b66c4d2df5606afa
push id129320
push usermjzffr@gmail.com
push dateFri, 23 Oct 2020 15:59:04 +0000
treeherderautoland@3a4b62af9597 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarionette-reviewers, jdescottes, whimboo
bugs1365886, 1672788
milestone84.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 1365886 - [marionette] Allow performActions to operate on chrome elements r=marionette-reviewers,jdescottes,whimboo Change error handling and initialization on the server side. On the Marionette Python client side, add a `kind` attribute to HTMLElement to distinguish chrome elements from content elements in the action sequence sent to the server. This change is necessary for `performActions` in contrast to other command implementations because of the extra parsing step done by `actions.Chain.fromJson` on the server side. Note that for the time being Marionette's ReferenceStore does not distinguish chrome and content elements (Bug 1672788), so this client-side change is correct but not strictly necessary. Differential Revision: https://phabricator.services.mozilla.com/D93778
testing/marionette/action.js
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.py
testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
--- a/testing/marionette/action.js
+++ b/testing/marionette/action.js
@@ -362,17 +362,17 @@ action.specCompatPointerOrigin = true;
 action.PointerOrigin.get = function(obj) {
   let origin = obj;
   if (typeof obj == "undefined") {
     origin = this.Viewport;
   } else if (typeof obj == "string") {
     let name = capitalize(obj);
     assert.in(name, this, pprint`Unknown pointer-move origin: ${obj}`);
     origin = this[name];
-  } else if (!element.isDOMElement(obj)) {
+  } else if (!element.isElement(obj)) {
     throw new error.InvalidArgumentError(
       "Expected 'origin' to be undefined, " +
         '"viewport", "pointer", ' +
         pprint`or an element, got: ${obj}`
     );
   }
   return origin;
 };
@@ -403,28 +403,28 @@ action.PointerType.get = function(str) {
   return this[name];
 };
 
 /**
  * Input state associated with current session.  This is a map between
  * input ID and the device state for that input source, with one entry
  * for each active input source.
  *
- * Initialized in listener.js.
+ * Re-initialized in listener.js.
  */
-action.inputStateMap = undefined;
+action.inputStateMap = new Map();
 
 /**
  * List of {@link action.Action} associated with current session.  Used to
  * manage dispatching events when resetting the state of the input sources.
  * Reset operations are assumed to be idempotent.
  *
- * Initialized in listener.js
+ * Re-initialized in listener.js
  */
-action.inputsToCancel = undefined;
+action.inputsToCancel = [];
 
 /**
  * Represents device state for an input source.
  */
 class InputState {
   constructor() {
     this.type = this.constructor.name.toLowerCase();
   }
@@ -1491,16 +1491,16 @@ function capitalize(str) {
 function inViewPort(x, y, win) {
   assert.number(x, `Expected x to be finite number`);
   assert.number(y, `Expected y to be finite number`);
   // Viewport includes scrollbars if rendered.
   return !(x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight);
 }
 
 function getElementCenter(el, win) {
-  if (element.isDOMElement(el)) {
+  if (element.isElement(el)) {
     if (action.specCompatPointerOrigin) {
       return element.getInViewCentrePoint(el.getClientRects()[0], win);
     }
     return element.coordinates(el);
   }
   return {};
 }
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -98,17 +98,17 @@ class ActionSequence(object):
             "type": "pointerMove",
             "x": x,
             "y": y
         }
         if duration is not None:
             action["duration"] = duration
         if origin is not None:
             if isinstance(origin, HTMLElement):
-                action["origin"] = {WEB_ELEMENT_KEY: origin.id}
+                action["origin"] = {origin.kind: origin.id}
             else:
                 action["origin"] = origin
         self._actions.append(action)
         return self
 
     def pointer_up(self, button=MouseButton.LEFT):
         """Queue a pointerUp action for `button`.
 
@@ -193,20 +193,21 @@ class Actions(object):
         return ActionSequence(self.marionette, *args, **kwargs)
 
 
 class HTMLElement(object):
     """Represents a DOM Element."""
 
     identifiers = (CHROME_ELEMENT_KEY, FRAME_KEY, WINDOW_KEY, WEB_ELEMENT_KEY)
 
-    def __init__(self, marionette, id):
+    def __init__(self, marionette, id, kind=WEB_ELEMENT_KEY):
         self.marionette = marionette
         assert(id is not None)
         self.id = id
+        self.kind = kind
 
     def __str__(self):
         return self.id
 
     def __eq__(self, other_element):
         return self.id == other_element.id
 
     def find_element(self, method, target):
@@ -342,23 +343,23 @@ class HTMLElement(object):
         body = {"id": self.id, "propertyName": property_name}
         return self.marionette._send_message("WebDriver:GetElementCSSValue",
                                              body, key="value")
 
     @classmethod
     def _from_json(cls, json, marionette):
         if isinstance(json, dict):
             if WEB_ELEMENT_KEY in json:
-                return cls(marionette, json[WEB_ELEMENT_KEY])
+                return cls(marionette, json[WEB_ELEMENT_KEY], WEB_ELEMENT_KEY)
             elif CHROME_ELEMENT_KEY in json:
-                return cls(marionette, json[CHROME_ELEMENT_KEY])
+                return cls(marionette, json[CHROME_ELEMENT_KEY], CHROME_ELEMENT_KEY)
             elif FRAME_KEY in json:
-                return cls(marionette, json[FRAME_KEY])
+                return cls(marionette, json[FRAME_KEY], FRAME_KEY)
             elif WINDOW_KEY in json:
-                return cls(marionette, json[WINDOW_KEY])
+                return cls(marionette, json[WINDOW_KEY], WINDOW_KEY)
         raise ValueError("Unrecognised web element")
 
 
 class Alert(object):
     """A class for interacting with alerts.
 
     ::
 
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1925,53 +1925,57 @@ GeckoDriver.prototype.singleTap = async 
  * @throws {NoSuchWindowError}
  *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not yet available in current context.
  */
 GeckoDriver.prototype.performActions = async function(cmd) {
-  assert.content(
-    this.context,
-    "Command 'performActions' is not yet available in chrome context"
-  );
   assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   const actions = cmd.parameters.actions;
 
   if (MarionettePrefs.useActors) {
     await this.getActor().performActions(actions, this.capabilities);
     return;
   }
 
+  assert.content(
+    this.context,
+    "Command 'performActions' is not yet available in chrome context"
+  );
+
   await this.listener.performActions({ actions }, this.capabilities);
 };
 
 /**
  * Release all the keys and pointer buttons that are currently depressed.
  *
  * @throws {NoSuchWindowError}
  *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not available in current context.
  */
 GeckoDriver.prototype.releaseActions = async function() {
-  assert.content(this.context);
   assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   if (MarionettePrefs.useActors) {
     await this.getActor().releaseActions();
     return;
   }
 
+  assert.content(
+    this.context,
+    "Command 'releaseActions' is not yet available in chrome context"
+  );
   await this.listener.releaseActions();
 };
 
 /**
  * An action chain.
  *
  * @param {Object} value
  *     A nested array where the inner array represents each event,
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.py
@@ -0,0 +1,63 @@
+# 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 __future__ import absolute_import
+
+from marionette_driver import By, errors
+from marionette_driver.keys import Keys
+
+from marionette_harness import MarionetteTestCase, WindowManagerMixin
+
+
+class TestPointerActions(WindowManagerMixin, MarionetteTestCase):
+
+    def setUp(self):
+        super(TestPointerActions, self).setUp()
+
+        self.actors_enabled = self.marionette.get_pref("marionette.actors.enabled")
+
+        self.mouse_chain = self.marionette.actions.sequence(
+            "pointer", "pointer_id", {"pointerType": "mouse"})
+        self.key_chain = self.marionette.actions.sequence(
+            "key", "keyboard_id")
+
+        if self.marionette.session_capabilities["platformName"] == "mac":
+            self.mod_key = Keys.META
+        else:
+            self.mod_key = Keys.CONTROL
+
+        self.marionette.set_context("chrome")
+
+        self.win = self.open_chrome_window("chrome://marionette/content/test.xhtml")
+        self.marionette.switch_to_window(self.win)
+
+    def tearDown(self):
+        if self.actors_enabled:
+            self.marionette.actions.release()
+        self.close_all_windows()
+
+        super(TestPointerActions, self).tearDown()
+
+    def test_click_action(self):
+        box = self.marionette.find_element(By.ID, "testBox")
+        box.get_property("localName")
+        if self.actors_enabled:
+            self.assertFalse(self.marionette.execute_script(
+                "return document.getElementById('testBox').checked"))
+            self.mouse_chain.click(element=box).perform()
+            self.assertTrue(self.marionette.execute_script(
+                "return document.getElementById('testBox').checked"))
+        else:
+            with self.assertRaises(errors.UnsupportedOperationException):
+                self.mouse_chain.click(element=box).perform()
+
+    def test_key_action(self):
+        self.marionette.find_element(By.ID, "textInput").click()
+        if self.actors_enabled:
+            self.key_chain.send_keys("x").perform()
+            self.assertEqual(self.marionette.execute_script(
+                "return document.getElementById('textInput').value"), "testx")
+        else:
+            with self.assertRaises(errors.UnsupportedOperationException):
+                self.key_chain.send_keys("x").perform()
--- a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.ini
@@ -75,16 +75,17 @@ skip-if = manage_instance == false || (d
 skip-if = manage_instance == false
 [test_context.py]
 
 [test_modal_dialogs.py]
 [test_unhandled_prompt_behavior.py]
 
 [test_key_actions.py]
 [test_mouse_action.py]
+[test_chrome_action.py]
 
 [test_teardown_context_preserved.py]
 [test_file_upload.py]
 skip-if = os == "win" # http://bugs.python.org/issue14574
 
 [test_execute_sandboxes.py]
 [test_prefs.py]
 [test_prefs_enforce.py]