Bug 845925 - Add scroll/pinch actions using touch action chains to the python touch layer, r=mdas
authorYiming Yang <yiyang@mozilla.com>
Fri, 26 Apr 2013 11:14:50 -0700
changeset 130124 1a16a93a396bb1561e0c6f4569222d5386065cfe
parent 130123 ae2ec3465251b19b220b56a34592098d443270b3
child 130125 54106990fcd8f2791b423aa5c6ffbc2b6924914c
push id24599
push userryanvm@gmail.com
push dateSun, 28 Apr 2013 01:24:06 +0000
treeherdermozilla-central@9d8977cbbfc6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmdas
bugs845925
milestone23.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 845925 - Add scroll/pinch actions using touch action chains to the python touch layer, r=mdas
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_getstatus.py]
 [test_import_script.py]
 [test_import_script_content.py.py]
 b2g = false
 [test_click.py]
 b2g = false
 [test_selected.py]
 b2g = false
@@ -43,36 +46,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
@@ -742,17 +742,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;