Bug 845925 - Add scroll/pinch actions using touch action chains to the python touch layer. r=mdas, a=test-only
☠☠ backed out by 17daac8919eb ☠ ☠
authorYiming Yang <yiyang@mozilla.com>
Fri, 26 Apr 2013 11:14:50 -0700
changeset 119242 6ac1987eb6ca2e171c183cc933cdc27aaeb6acc8
parent 119241 831b2ac43d55e1c2975c24e78a3b02904b01c024
child 119243 a227aa221cc5866af0032b368756e910b35c8391
push id744
push userryanvm@gmail.com
push dateWed, 01 May 2013 20:00:50 +0000
reviewersmdas, test-only
bugs845925
milestone18.0
Bug 845925 - Add scroll/pinch actions using touch action chains to the python touch layer. r=mdas, a=test-only
testing/marionette/client/marionette/gestures.py
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/marionette_touch.py
testing/marionette/client/marionette/tests/unit/test_gesture.py
testing/marionette/client/marionette/tests/unit/test_single_finger.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/client/marionette/www/testAction.html
testing/marionette/marionette-listener.js
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/gestures.py
@@ -0,0 +1,71 @@
+# 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 import MultiActions, Actions
+
+#axis is y or x
+#direction is 0 for positive, and -1 for negative
+#length is the total length we want to scroll
+#increments is how much we want to move per scrolling
+#wait_period is the seconds we wait between scrolling
+#scroll_back is whether we want to scroll back to original view
+def smooth_scroll(marionette_session, start_element, axis, direction, length, increments=None, wait_period=None, scroll_back=None):
+    if axis not in ["x", "y"]:
+        raise Exception("Axis must be either 'x' or 'y'")
+    if direction not in [-1, 0]:
+        raise Exception("Direction must either be -1 negative or 0 positive")
+    increments = increments or 100
+    wait_period = wait_period or 0.05
+    scroll_back = scroll_back or False
+    current = 0
+    if axis is "x":
+        if direction is -1:
+            offset = [-increments, 0]
+        else:
+            offset = [increments, 0]
+    else:
+        if direction is -1:
+            offset = [0, -increments]
+        else:
+            offset = [0, increments]
+    action = Actions(marionette_session)
+    action.press(start_element)
+    while (current < length):
+        current += increments
+        action.move_by_offset(*offset).wait(wait_period)
+    if scroll_back:
+        offset = [-value for value in offset]
+        while (current > 0):
+            current -= increments
+            action.move_by_offset(*offset).wait(wait_period)
+    action.release()
+    action.perform()
+
+#element is the target
+#x1,x2 are 1st finger starting position relative to the target
+#x3,y3 are 1st finger ending position relative to the target
+#x2,y2 are 2nd finger starting position relative to the target
+#x4,y4 are 2nd finger ending position relative to the target
+#duration is the amount of time in milliseconds we want to complete the pinch.
+def pinch(marionette_session, element, x1, y1, x2, y2, x3, y3, x4, y4, duration=200):
+    time = 0
+    time_increment = 10
+    if time_increment >= duration:
+        time_increment = duration
+    move_x1 = time_increment*1.0/duration * (x3 - x1)
+    move_y1 = time_increment*1.0/duration * (y3 - y1)
+    move_x2 = time_increment*1.0/duration * (x4 - x2)
+    move_y2 = time_increment*1.0/duration * (y4 - y2)
+    multiAction = MultiActions(marionette_session)
+    action1 = Actions(marionette_session)
+    action2 = Actions(marionette_session)
+    action1.press(element, x1, y1)
+    action2.press(element, x2, y2)
+    while (time < duration):
+        time += time_increment
+        action1.move_by_offset(move_x1, move_y1).wait(time_increment/1000)
+        action2.move_by_offset(move_x2, move_y2).wait(time_increment/1000)
+    action1.release()
+    action2.release()
+    multiAction.add(action1).add(action2).perform()
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -133,16 +133,32 @@ class Actions(object):
     def wait(self, time=None):
         self.action_chain.append(['wait', time])
         return self
 
     def cancel(self):
         self.action_chain.append(['cancel'])
         return self
 
+    def flick(self, element, x1, y1, x2, y2, duration=200):
+        element = element.id
+        time = 0
+        time_increment = 10
+        if time_increment >= duration:
+            time_increment = duration
+        move_x = time_increment*1.0/duration * (x2 - x1)
+        move_y = time_increment*1.0/duration * (y2 - y1)
+        self.action_chain.append(['press', element, x1, y1])
+        while (time < duration):
+            time += time_increment
+            self.action_chain.append(['moveByOffset', move_x, move_y])
+            self.action_chain.append(['wait', time_increment/1000])
+        self.action_chain.append(['release'])
+        return self
+
     def long_press(self, element, time_in_seconds):
         element = element.id
         self.action_chain.append(['press', element])
         self.action_chain.append(['wait', time_in_seconds])
         self.action_chain.append(['release'])
         return self
 
     def perform(self):
--- a/testing/marionette/client/marionette/marionette_touch.py
+++ b/testing/marionette/client/marionette/marionette_touch.py
@@ -1,15 +1,16 @@
 # 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/.
 
 import os
 from errors import ElementNotVisibleException
-
+from marionette import Actions
+from gestures import pinch
 """
 Adds touch support in Marionette
 """
 class MarionetteTouchMixin(object):
     """
     Set up the touch layer.
     Can specify another library with a path and the name of the library.
     """
@@ -30,13 +31,14 @@ class MarionetteTouchMixin(object):
         self.execute_script("%s.tap(arguments[0], null, null, null, null, arguments[1]);" % self.library_name, [element, send_all])
 
     def double_tap(self, element):
         self.check_element(element)
         self.execute_script("%s.dbltap(arguments[0]);" % self.library_name, [element])
 
     def flick(self, element, x1, y1, x2, y2, duration=200):
         self.check_element(element)
-        # there's 'flick' which is pixels per second, but I'd rather have the library support it than piece it together here.
-        self.execute_script("%s.swipe.apply(this, arguments);" % self.library_name, [element, x1, y1, x2, y2, duration])
+        action = Actions(self.marionette)
+        action.flick(element, x1, y1, x2, y2, duration).perform()
 
-    def pinch(self, *args, **kwargs):
-        raise Exception("Pinch is unsupported")
+    def pinch(self, element, x1, y1, x2, y2, x3, y3, x4, y4, duration = 200):
+        self.check_element(element)
+        pinch(element, x1, y1, x2, y2, x3, y3, x4, y4, duration)
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_gesture.py
@@ -0,0 +1,23 @@
+# 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/.
+
+import time
+from marionette_test import MarionetteTestCase
+from gestures import smooth_scroll, pinch
+
+class testGestures(MarionetteTestCase):
+    def test_smooth_scroll(self):
+        testTouch = self.marionette.absolute_url("testAction.html")
+        self.marionette.navigate(testTouch)
+        button = self.marionette.find_element("id", "mozLinkScrollStart")
+        smooth_scroll(self.marionette, button, "y",  -1, 250)
+        time.sleep(15)
+        self.assertEqual("End", self.marionette.execute_script("return document.getElementById('mozLinkScroll').innerHTML;"))
+
+    def test_pinch(self):
+        testTouch = self.marionette.absolute_url("testAction.html")
+        self.marionette.navigate(testTouch)
+        button = self.marionette.find_element("id", "mozLinkScrollStart")
+        pinch(self.marionette, button, 0, 0, 0, 0, 0, -50, 0, 50)
+        time.sleep(15)
--- a/testing/marionette/client/marionette/tests/unit/test_single_finger.py
+++ b/testing/marionette/client/marionette/tests/unit/test_single_finger.py
@@ -1,16 +1,17 @@
 # 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/.
 
 import time
 from marionette_test import MarionetteTestCase
 from marionette import Actions
 from errors import NoSuchElementException, MarionetteException
+from unittest import skip
 
 class testSingleFinger(MarionetteTestCase):
     def test_wait(self):
         testTouch = self.marionette.absolute_url("testAction.html")
         self.marionette.navigate(testTouch)
         button = self.marionette.find_element("id", "mozLinkCopy")
         action = Actions(self.marionette)
         action.press(button).wait(0.2).release()
@@ -126,21 +127,32 @@ class testSingleFinger(MarionetteTestCas
         testTouch = self.marionette.absolute_url("testAction.html")
         self.marionette.navigate(testTouch)
         button = self.marionette.find_element("id", "mozLinkCopy")
         action = Actions(self.marionette)
         action.long_press(button, 5).perform()
         time.sleep(10)
         self.assertEqual("ContextEnd", self.marionette.execute_script("return document.getElementById('mozLinkCopy').innerHTML;"))
 
+    @skip("Skipping due to Bug 865334")
     def test_long_press_fail(self):
         testTouch = self.marionette.absolute_url("testAction.html")
         self.marionette.navigate(testTouch)
         button = self.marionette.find_element("id", "mozLinkCopy")
         action = Actions(self.marionette)
         action.press(button).long_press(button, 5)
         self.assertRaises(MarionetteException, action.perform)
 
     def test_wrong_value(self):
         testTouch = self.marionette.absolute_url("testAction.html")
         self.marionette.navigate(testTouch)
         self.assertRaises(MarionetteException, self.marionette.send_mouse_event, "boolean")
 
+    def test_chain_flick(self):
+        testTouch = self.marionette.absolute_url("testAction.html")
+        self.marionette.navigate(testTouch)
+        button = self.marionette.find_element("id", "mozLinkScrollStart")
+        action = Actions(self.marionette)
+        action.flick(button, 0, 0, 0, -250).perform()
+        time.sleep(15)
+        self.assertEqual("End", self.marionette.execute_script("return document.getElementById('mozLinkScroll').innerHTML;"))
+        self.assertEqual("Start", self.marionette.execute_script("return document.getElementById('mozLinkScrollStart').innerHTML;"))
+
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -6,16 +6,19 @@ qemu = false
 browser = true
 
 ; true if the test is compatible with b2g, otherwise false
 b2g = true
 
 ; true if the test should be skipped
 skip = false
 
+; true if the test requires unagi
+unagi = false
+
 [test_import_script.py]
 [test_import_script_content.py.py]
 b2g = false
 [test_click.py]
 b2g = false
 [test_selected.py]
 b2g = false
 [test_getattr.py]
@@ -41,36 +44,41 @@ b2g = false
 
 [test_timeouts.py]
 b2g = false
 
 [test_touch.py]
 b2g = true
 browser = false
 
-[test_cancel.py]
+[test_press_release.py]
 b2g = true
 browser = false
 
-[test_press_release.py]
+[test_gesture.py]
 b2g = true
 browser = false
+unagi = true
 
 [test_single_finger.py]
 b2g = true
 browser = false
 
 [test_multi_finger.py]
 b2g = true
 browser = false
 
 [test_tap.py]
 b2g = true
 browser = false
 
+[test_cancel.py]
+b2g = true
+browser = false
+
 [test_simpletest_pass.js]
 [test_simpletest_sanity.py]
 [test_simpletest_chrome.js]
 [test_simpletest_timeout.js]
 [test_specialpowers.py]
 [test_switch_frame.py]
 b2g = false
 
--- a/testing/marionette/client/marionette/www/testAction.html
+++ b/testing/marionette/client/marionette/www/testAction.html
@@ -10,16 +10,20 @@
 </head>
 <body>
   <h1 id="testh1">Test Page</h1>
   <!-- "mozLink" and "mozLinkPos" work together to perform touchdown on mozLink, vertical move and then touchup on mozLinkPos-->
   <button id="mozLink" style="position:absolute;left:0px;top:55px;" type="button" allowevents=true>Button1</button>
   <button id="mozLinkPos" style="position:absolute;left:0px;top:355px;" type="button" allowevents=true>Button2</button>
   <!-- "mozLinkCopy" listens for a touchdown and touchup -->
   <button id="mozLinkCopy" style="position:absolute;left:0px;top:455px;" type="button" allowevents=true>Button3</button>
+  <!-- "mozLinkScroll" listens for scroll -->
+  <button id="mozLinkScroll" style="position:absolute;left:0px;top:655px;" type="button" allowevents=true>Button8</button>
+  <!-- "mozLinkScrollStart" listens for scroll -->
+  <button id="mozLinkScrollStart" style="position:absolute;left:0px;top:405px;" type="button" allowevents=true>Button9</button>
   <!-- "mozLinkStart" and "mozLinkEnd" work together to perform touchdown on mozLinkStart, horizontal move and then touchup on mozLinkEnd -->  
   <button id="mozLinkStart" style="position:absolute;left:10px;top:200px;" type="button" allowevents=true>Press</button>
   <button id="mozLinkEnd" style="position:absolute;left:140px;top:200px;" type="button" allowevents=true>Release</button>
   <!-- "mozLinkCopy2" listens for a touchdown and touchup. It shows the time when it's fired-->
   <button id="mozLinkCopy2" style="position:absolute;left:80px;top:455px;" type="button" allowevents=true>Button4</button>
   <!-- "mozLinkCancel" listens for a touchdown and touchcancel -->
   <button id="mozLinkCancel" style="position:absolute;left:0px;top:255px;" type="button" allowevents=true>Button5</button>
   <!-- "mozMouse" listens for mouse events -->
@@ -27,16 +31,17 @@
   <script type="text/javascript">
     window.ready = true;
     var press = document.getElementById("mozLink");
     var second = document.getElementById("mozLinkCopy");
     var third = document.getElementById("mozLinkStart");
     var fourth = document.getElementById("mozLinkCopy2");
     var fifth = document.getElementById("mozLinkCancel");
     var sixth = document.getElementById("mozMouse");
+    var seventh = document.getElementById("mozLinkScrollStart");
     // touchmove and touchend must be performed on the same element as touchstart
     // here is press for vertical move
     press.addEventListener("touchstart", function(){changePressText("mozLink")}, false);
     press.addEventListener("touchmove", changeMoveText, false);
     press.addEventListener("touchend", changeReleaseText, false);
     // here is second for a tap
     second.addEventListener("touchstart", function(){changePressText("mozLinkCopy")}, false);
     second.addEventListener("touchend", function(){changeClickText("mozLinkCopy")}, false);
@@ -54,16 +59,19 @@
     fifth.addEventListener("touchcancel", function(){changeClickText("mozLinkCancel")}, false);
     // here is sixth for mouse event
     sixth.addEventListener("touchstart", function(){changeMouseText("TouchStart")}, false);
     sixth.addEventListener("touchend", function(){changeMouseText("TouchEnd")}, false);
     sixth.addEventListener("mousemove", function(){changeMouseText("MouseMove")}, false);
     sixth.addEventListener("mousedown", function(){changeMouseText("MouseDown")}, false);
     sixth.addEventListener("mouseup", function(){changeMouseText("MouseUp")}, false);
     sixth.addEventListener("click", function(){changeMouseText("MouseClick")}, false);
+    // here is seventh for a scroll
+    seventh.addEventListener("touchstart", function(){changePressText("mozLinkScrollStart")}, false);
+    seventh.addEventListener("touchend", function(){changeScrollText("mozLinkScroll")}, false);
     function changeMouseText(strId) {
       var mouse = document.getElementById("mozMouse");
       switch(strId) {
         case "TouchStart":
           if (mouse.innerHTML == "MouseClick") {
             mouse.innerHTML = "TouchStart2";
           }
           else if (mouse.innerHTML == "TouchEnd") {
@@ -188,23 +196,33 @@
       else if (second.innerHTML == "Context") {
         second.innerHTML = "ContextEnd";
       }
       else {
         second.innerHTML = "Error";
       }
     }
 
+    function changeScrollText(strId) {
+      var seventh = document.getElementById(strId);
+      if (elementInViewport(seventh)) {
+        seventh.innerHTML = "End";
+      }
+      else {
+        seventh.innerHTML = "Error";
+      }
+    }
+
     function changeTimePress() {
       var fourth = document.getElementById("mozLinkCopy2");
       var d = new Date();
       fourth.innerHTML = d.getTime();
       var newButton = document.createElement("button");
       newButton.id = "delayed";
-      newButton.setAttribute("style", "position:absolute;left:80px;top:105px;");
+      newButton.setAttribute("style", "position:absolute;left:220px;top:455px;");
       var content = document.createTextNode("Button6");
       newButton.appendChild(content);
       document.body.appendChild(newButton);
     }
 
     function changeTimeRelease(event) {
       var fourth = document.getElementById("mozLinkCopy2");
       if (fourth.innerHTML != "Button4") {
@@ -219,11 +237,27 @@
         document.getElementById("delayed").innerHTML = "End";
       }
     }
 
     function onContextMenuChange() {
       var context = document.getElementById("mozLinkCopy");
       context.innerHTML = "Context";
     }
+
+    function elementInViewport(el) {
+      var top = el.offsetTop;
+      var left = el.offsetLeft;
+      var width = el.offsetWidth;
+      var height = el.offsetHeight;
+      while(el.offsetParent) {
+        el = el.offsetParent;
+        top += el.offsetTop;
+        left += el.offsetLeft;
+      }
+      return (top >= window.pageYOffset &&
+              left >= window.pageXOffset &&
+              (top + height) <= (window.pageYOffset + window.innerHeight) &&
+              (left + width) <= (window.pageXOffset + window.innerWidth));
+    }
   </script>
 </body>
 </html>
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -730,17 +730,17 @@ function touch(target, duration, xt, yt,
     else {
       checkTimer.initWithCallback(nextEvent, EVENT_INTERVAL, Ci.nsITimer.TYPE_ONE_SHOT);
     }
   }
 }
 
 /**
  * This function generates the coordinates of the element
- * @param 'x0', 'y0', 'x1', and 'y1' are the relative to the viewport.
+ * @param 'x0', 'y0', 'x1', and 'y1' are the relative to the target.
  *        If they are not specified, then the center of the target is used.
  */
 function coordinates(target, x0, y0, x1, y1) {
   let coords = {};
   let box = target.getBoundingClientRect();
   let tx0 = typeof x0;
   let ty0 = typeof y0;
   let tx1 = typeof x1;