Bug 833448 - add singleTap and doubleTap ability to marionette, r=mdas
authorYiming Yang <yiyang@mozilla.com>
Tue, 22 Jan 2013 11:27:44 -0800
changeset 130362 ccdcf297a43fe35a8155b7549c9de2df5578c630
parent 130361 9aadf7e35e705869d01dd38a810361b5105fd934
child 130363 efc9d9f5f30ee04ced4ce563d7f50a8e6cf97a5b
push id2323
push userbbajaj@mozilla.com
push dateMon, 01 Apr 2013 19:47:02 +0000
treeherdermozilla-beta@7712be144d91 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmdas
bugs833448
milestone21.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 833448 - add singleTap and doubleTap ability to marionette, r=mdas
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/tests/unit/test_touch.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/client/marionette/www/shim.js
testing/marionette/client/marionette/www/testTouch.html
testing/marionette/marionette-actors.js
testing/marionette/marionette-listener.js
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -45,16 +45,22 @@ class HTMLElement(object):
         return self.marionette.find_elements(method, target, self.id)
 
     def get_attribute(self, attribute):
         return self.marionette._send_message('getElementAttribute', 'value', element=self.id, name=attribute)
 
     def click(self):
         return self.marionette._send_message('clickElement', 'ok', element=self.id)
 
+    def single_tap(self):
+        return self.marionette._send_message('singleTap', 'ok', element=self.id)
+
+    def double_tap(self):
+        return self.marionette._send_message('doubleTap', 'ok', element=self.id)
+
     @property
     def text(self):
         return self.marionette._send_message('getElementText', 'value', element=self.id)
 
     def send_keys(self, *string):
         typing = []
         for val in string:
             if isinstance(val, Keys):
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_touch.py
@@ -0,0 +1,32 @@
+# 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
+import time
+from marionette_test import MarionetteTestCase
+from marionette import HTMLElement
+from errors import MarionetteException
+
+class testTouch(MarionetteTestCase):
+    def test_touch(self):
+      testTouch = self.marionette.absolute_url("testTouch.html")
+      self.marionette.navigate(testTouch)
+      button = self.marionette.find_element("id", "mozLink")
+      button.single_tap()
+      time.sleep(10)
+      self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;"))
+
+    def test_invisible(self):
+      testTouch = self.marionette.absolute_url("testTouch.html")
+      self.marionette.navigate(testTouch)
+      ele = self.marionette.find_element("id", "testh2")
+      self.assertRaises(MarionetteException, ele.single_tap)
+
+    def test_scrolling(self):
+      testTouch = self.marionette.absolute_url("testTouch.html")
+      self.marionette.navigate(testTouch)
+      ele = self.marionette.find_element("id", "scroll")
+      ele.single_tap()
+      time.sleep(10)
+      self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('scroll').innerHTML;"))
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -39,16 +39,20 @@ qemu = true
 b2g = false
 
 [test_navigation.py]
 b2g = false
 
 [test_timeouts.py]
 b2g = false
 
+[test_touch.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
 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/www/shim.js
@@ -0,0 +1,282 @@
+/**
+* mouse_event_shim.js: generate mouse events from touch events.
+*
+* This library listens for touch events and generates mousedown, mousemove
+* mouseup, and click events to match them. It captures and dicards any
+* real mouse events (non-synthetic events with isTrusted true) that are
+* send by gecko so that there are not duplicates.
+*
+* This library does emit mouseover/mouseout and mouseenter/mouseleave
+* events. You can turn them off by setting MouseEventShim.trackMouseMoves to
+* false. This means that mousemove events will always have the same target
+* as the mousedown even that began the series. You can also call
+* MouseEventShim.setCapture() from a mousedown event handler to prevent
+* mouse tracking until the next mouseup event.
+*
+* This library does not support multi-touch but should be sufficient
+* to do drags based on mousedown/mousemove/mouseup events.
+*
+* This library does not emit dblclick events or contextmenu events
+*/
+
+'use strict';
+
+(function() {
+  // Make sure we don't run more than once
+  if (MouseEventShim)
+    return;
+
+  // Bail if we're not on running on a platform that sends touch
+  // events. We don't need the shim code for mouse events.
+  try {
+    document.createEvent('TouchEvent');
+  } catch (e) {
+    return;
+  }
+
+  var starttouch; // The Touch object that we started with
+  var target; // The element the touch is currently over
+  var emitclick; // Will we be sending a click event after mouseup?
+
+  // Use capturing listeners to discard all mouse events from gecko
+  window.addEventListener('mousedown', discardEvent, true);
+  window.addEventListener('mouseup', discardEvent, true);
+  window.addEventListener('mousemove', discardEvent, true);
+  window.addEventListener('click', discardEvent, true);
+
+  function discardEvent(e) {
+    if (e.isTrusted) {
+      e.stopImmediatePropagation(); // so it goes no further
+      if (e.type === 'click')
+        e.preventDefault(); // so it doesn't trigger a change event
+    }
+  }
+
+  // Listen for touch events that bubble up to the window.
+  // If other code has called stopPropagation on the touch events
+  // then we'll never see them. Also, we'll honor the defaultPrevented
+  // state of the event and will not generate synthetic mouse events
+  window.addEventListener('touchstart', handleTouchStart);
+  window.addEventListener('touchmove', handleTouchMove);
+  window.addEventListener('touchend', handleTouchEnd);
+  window.addEventListener('touchcancel', handleTouchEnd); // Same as touchend
+
+  function handleTouchStart(e) {
+    // If we're already handling a touch, ignore this one
+    if (starttouch)
+      return;
+
+    // Ignore any event that has already been prevented
+    if (e.defaultPrevented)
+      return;
+
+    // Sometimes an unknown gecko bug causes us to get a touchstart event
+    // for an iframe target that we can't use because it is cross origin.
+    // Don't start handling a touch in that case
+    try {
+      e.changedTouches[0].target.ownerDocument;
+    }
+    catch (e) {
+      // Ignore the event if we can't see the properties of the target
+      return;
+    }
+
+    // If there is more than one simultaneous touch, ignore all but the first
+    starttouch = e.changedTouches[0];
+    target = starttouch.target;
+    emitclick = true;
+
+    // Move to the position of the touch
+    emitEvent('mousemove', target, starttouch);
+
+    // Now send a synthetic mousedown
+    var result = emitEvent('mousedown', target, starttouch);
+
+    // If the mousedown was prevented, pass that on to the touch event.
+    // And remember not to send a click event
+    if (!result) {
+      e.preventDefault();
+      emitclick = false;
+    }
+  }
+
+  function handleTouchEnd(e) {
+    if (!starttouch)
+      return;
+
+    // End a MouseEventShim.setCapture() call
+    if (MouseEventShim.capturing) {
+      MouseEventShim.capturing = false;
+      MouseEventShim.captureTarget = null;
+    }
+
+    for (var i = 0; i < e.changedTouches.length; i++) {
+      var touch = e.changedTouches[i];
+      // If the ended touch does not have the same id, skip it
+      if (touch.identifier !== starttouch.identifier)
+        continue;
+
+      emitEvent('mouseup', target, touch);
+
+      // If target is still the same element we started and the touch did not
+      // move more than the threshold and if the user did not prevent
+      // the mousedown, then send a click event, too.
+      if (emitclick)
+        emitEvent('click', starttouch.target, touch);
+
+      starttouch = null;
+      return;
+    }
+  }
+
+  function handleTouchMove(e) {
+    if (!starttouch)
+      return;
+
+    for (var i = 0; i < e.changedTouches.length; i++) {
+      var touch = e.changedTouches[i];
+      // If the ended touch does not have the same id, skip it
+      if (touch.identifier !== starttouch.identifier)
+        continue;
+
+      // Don't send a mousemove if the touchmove was prevented
+      if (e.defaultPrevented)
+        return;
+
+      // See if we've moved too much to emit a click event
+      var dx = Math.abs(touch.screenX - starttouch.screenX);
+      var dy = Math.abs(touch.screenY - starttouch.screenY);
+      if (dx > MouseEventShim.dragThresholdX ||
+          dy > MouseEventShim.dragThresholdY) {
+        emitclick = false;
+      }
+
+      var tracking = MouseEventShim.trackMouseMoves &&
+        !MouseEventShim.capturing;
+
+      if (tracking) {
+        // If the touch point moves, then the element it is over
+        // may have changed as well. Note that calling elementFromPoint()
+        // forces a layout if one is needed.
+        // XXX: how expensive is it to do this on each touchmove?
+        // Can we listen for (non-standard) touchleave events instead?
+        var oldtarget = target;
+        var newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
+        if (newtarget === null) {
+          // this can happen as the touch is moving off of the screen, e.g.
+          newtarget = oldtarget;
+        }
+        if (newtarget !== oldtarget) {
+          leave(oldtarget, newtarget, touch); // mouseout, mouseleave
+          target = newtarget;
+        }
+      }
+      else if (MouseEventShim.captureTarget) {
+        target = MouseEventShim.captureTarget;
+      }
+
+      emitEvent('mousemove', target, touch);
+
+      if (tracking && newtarget !== oldtarget) {
+        enter(newtarget, oldtarget, touch); // mouseover, mouseenter
+      }
+    }
+  }
+
+  // Return true if element a contains element b
+  function contains(a, b) {
+    return (a.compareDocumentPosition(b) & 16) !== 0;
+  }
+
+  // A touch has left oldtarget and entered newtarget
+  // Send out all the events that are required
+  function leave(oldtarget, newtarget, touch) {
+    emitEvent('mouseout', oldtarget, touch, newtarget);
+
+    // If the touch has actually left oldtarget (and has not just moved
+    // into a child of oldtarget) send a mouseleave event. mouseleave
+    // events don't bubble, so we have to repeat this up the hierarchy.
+    for (var e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
+      emitEvent('mouseleave', e, touch, newtarget);
+    }
+  }
+
+  // A touch has entered newtarget from oldtarget
+  // Send out all the events that are required.
+  function enter(newtarget, oldtarget, touch) {
+    emitEvent('mouseover', newtarget, touch, oldtarget);
+
+    // Emit non-bubbling mouseenter events if the touch actually entered
+    // newtarget and wasn't already in some child of it
+    for (var e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
+      emitEvent('mouseenter', e, touch, oldtarget);
+    }
+  }
+
+  function emitEvent(type, target, touch, relatedTarget) {
+    var synthetic = document.createEvent('MouseEvents');
+    var bubbles = (type !== 'mouseenter' && type !== 'mouseleave');
+    var count =
+      (type === 'mousedown' || type === 'mouseup' || type === 'click') ? 1 : 0;
+
+    synthetic.initMouseEvent(type,
+                             bubbles, // canBubble
+                             true, // cancelable
+                             window,
+                             count, // detail: click count
+                             touch.screenX,
+                             touch.screenY,
+                             touch.clientX,
+                             touch.clientY,
+                             false, // ctrlKey: we don't have one
+                             false, // altKey: we don't have one
+                             false, // shiftKey: we don't have one
+                             false, // metaKey: we don't have one
+                             0, // we're simulating the left button
+                             relatedTarget || null);
+
+    try {
+      return target.dispatchEvent(synthetic);
+    }
+    catch (e) {
+      console.warn('Exception calling dispatchEvent', type, e);
+      return true;
+    }
+  }
+}());
+
+var MouseEventShim = {
+  // It is a known gecko bug that synthetic events have timestamps measured
+  // in microseconds while regular events have timestamps measured in
+  // milliseconds. This utility function returns a the timestamp converted
+  // to milliseconds, if necessary.
+  getEventTimestamp: function(e) {
+    if (e.isTrusted) // XXX: Are real events always trusted?
+      return e.timeStamp;
+    else
+      return e.timeStamp / 1000;
+  },
+
+  // Set this to false if you don't care about mouseover/out events
+  // and don't want the target of mousemove events to follow the touch
+  trackMouseMoves: true,
+
+  // Call this function from a mousedown event handler if you want to guarantee
+  // that the mousemove and mouseup events will go to the same element
+  // as the mousedown even if they leave the bounds of the element. This is
+  // like setting trackMouseMoves to false for just one drag. It is a
+  // substitute for event.target.setCapture(true)
+  setCapture: function(target) {
+    this.capturing = true; // Will be set back to false on mouseup
+    if (target)
+      this.captureTarget = target;
+  },
+
+  capturing: false,
+
+  // Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
+  // If a touch ever moves more than this many pixels from its starting point
+  // then we will not synthesize a click event when the touch ends.
+  dragThresholdX: 25,
+  dragThresholdY: 25
+};
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/www/testTouch.html
@@ -0,0 +1,46 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+
+<html>
+<head>
+<script type="application/javascript" src="shim.js">
+</script>
+<title>Marionette Test</title>
+</head>
+<body>
+  <h1 id="testh1">Test Page</h1>
+  <script type="text/javascript">
+    window.ready = true;
+    setTimeout(addDelayedElement, 1000);  
+    function addDelayedElement() {
+      var newDiv = document.createElement("div");
+      newDiv.id = "newDiv";
+      var newContent = document.createTextNode("I am a newly created div!");
+      newDiv.appendChild(newContent);
+      document.body.appendChild(newDiv);
+    }
+    function clicked() {
+      var link = document.getElementById("mozLink");
+      link.innerHTML = "Clicked";
+    }
+    function clicked2() {
+      var link2 = document.getElementById("scroll");
+      link2.innerHTML = "Clicked";
+    }
+  </script>
+  <button id="mozLink" type="button" onclick="clicked()" allowevents=true>Click Me!</button>
+  <div id="testDiv">
+    <a href="#" id="divLink" class="linkClass" onclick="clicked()">Div click me!</a>
+    <a href="#" id="divLink2" class="linkClass" onclick="clicked()">Div click me!</a>
+  </div>
+  <input name="myInput" type="text" value="asdf"/>
+  <input name="myCheckBox" type="checkbox" />
+  <h2 id="testh2" style="visibility: hidden" class="linkClass">Hidden</h2>
+  <h3 id="testh3">Voluntary Termination</h3>
+  <br style="margin-bottom:600px;"/>
+  <button id="scroll" type="button" onclick="clicked2()" allowevents=true>Click Me!</button>
+</body>
+</html>
--- a/testing/marionette/marionette-actors.js
+++ b/testing/marionette/marionette-actors.js
@@ -1255,16 +1255,52 @@ MarionetteDriverActor.prototype = {
       else {
         this.pageTimeout = timeout;
         this.sendOk(this.command_id);
       }
     }
   },
 
   /**
+   * Single Tap
+   *
+   * @param object aRequest
+            'element' represents the ID of the element to single tap on
+   */
+  singleTap: function MDA_singleTap(aRequest) {
+    this.command_id = this.getCommandId();
+    let serId = aRequest.element;
+    if (this.context == "chrome") {
+      this.sendError("Not in Chrome", 500, null, this.command_id);
+    }
+    else {
+      this.sendAsync("singleTap", {value: serId,
+                                   command_id: this.command_id});
+    }
+  },
+
+  /**
+   * Double Tap
+   *
+   * @param object aRequest
+   *        'element' represents the ID of the element to double tap on
+   */
+  doubleTap: function MDA_doubleTap(aRequest) {
+    this.command_id = this.getCommandId();
+    let serId = aRequest.element;
+    if (this.context == "chrome") {
+      this.sendError("Not in Chrome", 500, null, this.command_id);
+    }
+    else {
+      this.sendAsync("doubleTap", {value: serId,
+                                   command_id: this.command_id});
+    }
+  },
+
+  /**
    * Find an element using the indicated search strategy.
    *
    * @param object aRequest
    *        'using' member indicates which search method to use
    *        'value' member is the value the client is looking for
    */
   findElement: function MDA_findElement(aRequest) {
     let command_id = this.command_id = this.getCommandId();
@@ -2004,16 +2040,18 @@ MarionetteDriverActor.prototype.requestT
   "log": MarionetteDriverActor.prototype.log,
   "getLogs": MarionetteDriverActor.prototype.getLogs,
   "addPerfData": MarionetteDriverActor.prototype.addPerfData,
   "getPerfData": MarionetteDriverActor.prototype.getPerfData,
   "setContext": MarionetteDriverActor.prototype.setContext,
   "executeScript": MarionetteDriverActor.prototype.execute,
   "setScriptTimeout": MarionetteDriverActor.prototype.setScriptTimeout,
   "timeouts": MarionetteDriverActor.prototype.timeouts,
+  "singleTap": MarionetteDriverActor.prototype.singleTap,
+  "doubleTap": MarionetteDriverActor.prototype.doubleTap,
   "executeAsyncScript": MarionetteDriverActor.prototype.executeWithCallback,
   "executeJSScript": MarionetteDriverActor.prototype.executeJSScript,
   "setSearchTimeout": MarionetteDriverActor.prototype.setSearchTimeout,
   "findElement": MarionetteDriverActor.prototype.findElement,
   "findElements": MarionetteDriverActor.prototype.findElements,
   "clickElement": MarionetteDriverActor.prototype.clickElement,
   "getElementAttribute": MarionetteDriverActor.prototype.getElementAttribute,
   "getElementText": MarionetteDriverActor.prototype.getElementText,
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -49,16 +49,22 @@ let sandbox;
 
 // Flag to indicate whether an async script is currently running or not.
 let asyncTestRunning = false;
 let asyncTestCommandId;
 let asyncTestTimeoutId;
 let originalOnError;
 //timer for doc changes
 let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+// Send move events about this often
+let EVENT_INTERVAL = 30; // milliseconds
+// The current array of all pending touches
+let touches = [];
+// For assigning unique ids to all touches
+let nextTouchId = 1000;
 
 /**
  * Called when listener is first started up. 
  * The listener sends its unique window ID and its current URI to the actor.
  * If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
   let msg = {value: winUtil.outerWindowID, href: content.location.href};
@@ -88,16 +94,18 @@ function removeMessageListenerId(message
 /**
  * Start all message listeners
  */
 function startListeners() {
   addMessageListenerId("Marionette:newSession", newSession);
   addMessageListenerId("Marionette:executeScript", executeScript);
   addMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript);
   addMessageListenerId("Marionette:executeJSScript", executeJSScript);
+  addMessageListenerId("Marionette:singleTap", singleTap);
+  addMessageListenerId("Marionette:doubleTap", doubleTap);
   addMessageListenerId("Marionette:setSearchTimeout", setSearchTimeout);
   addMessageListenerId("Marionette:goUrl", goUrl);
   addMessageListenerId("Marionette:getUrl", getUrl);
   addMessageListenerId("Marionette:getTitle", getTitle);
   addMessageListenerId("Marionette:getPageSource", getPageSource);
   addMessageListenerId("Marionette:goBack", goBack);
   addMessageListenerId("Marionette:goForward", goForward);
   addMessageListenerId("Marionette:refresh", refresh);
@@ -175,16 +183,18 @@ function restart(msg) {
 /**
  * Removes all listeners
  */
 function deleteSession(msg) {
   removeMessageListenerId("Marionette:newSession", newSession);
   removeMessageListenerId("Marionette:executeScript", executeScript);
   removeMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript);
   removeMessageListenerId("Marionette:executeJSScript", executeJSScript);
+  removeMessageListenerId("Marionette:singleTap", singleTap);
+  removeMessageListenerId("Marionette:doubleTap", doubleTap);
   removeMessageListenerId("Marionette:setSearchTimeout", setSearchTimeout);
   removeMessageListenerId("Marionette:goUrl", goUrl);
   removeMessageListenerId("Marionette:getTitle", getTitle);
   removeMessageListenerId("Marionette:getPageSource", getPageSource);
   removeMessageListenerId("Marionette:getUrl", getUrl);
   removeMessageListenerId("Marionette:goBack", goBack);
   removeMessageListenerId("Marionette:goForward", goForward);
   removeMessageListenerId("Marionette:refresh", refresh);
@@ -527,16 +537,264 @@ function executeWithCallback(msg, useFin
     Cu.evalInSandbox(scriptSrc, sandbox, "1.8");
   } catch (e) {
     // 17 = JavascriptException
     sandbox.asyncComplete(e.name + ': ' + e.message, 17);
   }
 }
 
 /**
+ * This function creates a touch event given a touch type and a touch
+ */
+function emitTouchEvent(type, touch) {
+  var target = touch.target;
+  var doc = target.ownerDocument;
+  var win = doc.defaultView;
+  // Using domWindowUtils
+  var domWindowUtils = curWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
+  domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.screenX], [touch.screenY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0);
+}
+
+/**
+ * This function creates a touch and emit touch events
+ * @param 'xt' and 'yt' are two-element array [from, to] and then is a callback that will be invoked after touchend event is sent
+ */
+function touch(target, duration, xt, yt, then) {
+  var doc = target.ownerDocument;
+  var win = doc.defaultView;
+  var touchId = nextTouchId++;
+  var x = xt;
+  if (typeof xt !== 'function') {
+    x = function(t) { return xt[0] + t / duration * (xt[1] - xt[0]); };
+  }
+  var y = yt;
+  if (typeof yt !== 'function') {
+    y = function(t) { return yt[0] + t / duration * (yt[1] - yt[0]); };
+  }
+  // viewport coordinates
+  var clientX = Math.round(x(0)), clientY = Math.round(y(0));
+  // document coordinates
+  var pageX = clientX + win.pageXOffset,
+      pageY = clientY + win.pageYOffset;
+  // screen coordinates
+  var screenX = clientX + win.mozInnerScreenX,
+      screenY = clientY + win.mozInnerScreenY;
+  // Remember the coordinates
+  var lastX = clientX, lastY = clientY;
+  // Create the touch object
+  var touch = doc.createTouch(win, target, touchId,
+                              pageX, pageY,
+                              screenX, screenY,
+                              clientX, clientY);
+  // Add this new touch to the list of touches
+  touches.push(touch);
+  // Send the start event
+  emitTouchEvent('touchstart', touch);
+  var startTime = Date.now();
+  checkTimer.initWithCallback(nextEvent, EVENT_INTERVAL, Ci.nsITimer.TYPE_ONE_SHOT);
+  function nextEvent() {
+  // Figure out if this is the last of the touchmove events
+    var time = Date.now();
+    var dt = time - startTime;
+    var last = dt + EVENT_INTERVAL / 2 > duration;
+    // Find our touch object in the touches[] array.
+    // Note that its index may have changed since we pushed it
+    var touchIndex = touches.indexOf(touch);
+    // If this is the last move event, make sure we move all the way
+    if (last)
+       dt = duration;
+    // New coordinates of the touch
+    clientX = Math.round(x(dt));
+    clientY = Math.round(y(dt));
+    // If we've moved, send a move event
+    if (clientX !== lastX || clientY !== lastY) { // If we moved
+      lastX = clientX;
+      lastY = clientY;
+      pageX = clientX + win.pageXOffset;
+      pageY = clientY + win.pageYOffset;
+      screenX = clientX + win.mozInnerScreenX;
+      screenY = clientY + win.mozInnerScreenY;
+      // Since we moved, we've got to create a new Touch object
+      // with the new coordinates
+      touch = doc.createTouch(win, target, touchId,
+                              pageX, pageY,
+                              screenX, screenY,
+                              clientX, clientY);
+      // Replace the old touch object with the new one
+      touches[touchIndex] = touch;
+      // And send the touchmove event
+      emitTouchEvent('touchmove', touch);
+    }
+    // If that was the last move, send the touchend event
+    // and call the callback 
+    if (last) {
+      touches.splice(touchIndex, 1);
+      emitTouchEvent('touchend', touch);
+      if (then)
+        checkTimer.initWithCallback(then, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+    }
+    // Otherwise, schedule the next event
+    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.
+ *        If they are not specified, then the center of the target is used.
+ */
+function coordinates(target, x0, y0, x1, y1) {
+  var coords = {};
+  var box = target.getBoundingClientRect();
+  var tx0 = typeof x0;
+  var ty0 = typeof y0;
+  var tx1 = typeof x1;
+  var ty1 = typeof y1; 
+  function percent(s, x) {
+    s = s.trim();
+    var f = parseFloat(s);
+    if (s[s.length - 1] === '%')
+      f = f * x / 100;
+      return f;
+  }
+  function relative(s, x) {
+    var factor;
+    if (s[0] === '+')
+      factor = 1;
+    else
+      factor = -1;
+      return factor * percent(s.substring(1), x);
+  }
+  if (tx0 === 'number')
+    coords.x0 = box.left + x0;
+  else if (tx0 === 'string')
+    coords.x0 = box.left + percent(x0, box.width);
+  //check tx1 point
+  if (tx1 === 'number')
+    coords.x1 = box.left + x1;
+  else if (tx1 === 'string') {
+    x1 = x1.trim();
+    if (x1[0] === '+' || x1[0] === '-')
+      coords.x1 = coords.x0 + relative(x1, box.width);
+    else
+      coords.x1 = box.left + percent(x1, box.width);
+  }
+  // check ty0
+  if (ty0 === 'number')
+    coords.y0 = box.top + y0;
+  else if (ty0 === 'string')
+    coords.y0 = box.top + percent(y0, box.height);
+  //check ty1
+  if (ty1 === 'number')
+    coords.y1 = box.top + y1;
+  else if (ty1 === 'string') {
+    y1 = y1.trim();
+    if (y1[0] === '+' || y1[0] === '-')
+      coords.y1 = coords.y0 + relative(y1, box.height);
+    else
+      coords.y1 = box.top + percent(y1, box.height);
+  }
+  return coords;
+}
+
+/**
+ * This function returns if the element is in viewport 
+ */
+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 >= curWindow.pageYOffset &&
+          left >= curWindow.pageXOffset &&
+          (top + height) <= (curWindow.pageYOffset + curWindow.innerHeight) &&
+          (left + width) <= (curWindow.pageXOffset + curWindow.innerWidth)
+         );
+}
+
+/**
+ * This function throws the visibility of the element error
+ */
+function checkVisible(el, command_id) {
+    //check if the element is visible
+    let visible = utils.isElementDisplayed(el);
+    if (!visible) {
+      return false;
+    }
+    //check if scroll function exist. If so, call it.
+    if (el.scrollIntoView) {
+      el.scrollIntoView(true);
+    }
+    var scroll = elementInViewport(el);
+    if (!scroll){
+      return false;
+    }
+    return true;
+}
+
+/**
+ * Function that perform a single tap
+ */
+function singleTap(msg) {
+  let command_id = msg.json.command_id;
+  let el;
+  try {
+    el = elementManager.getKnownElement(msg.json.value, curWindow);
+    if (!checkVisible(el, command_id)) {
+      sendError("Element is not currently visible and may not be manipulated", 11, null, command_id);
+      return;
+    }
+    let x = '50%';
+    let y = '50%';
+    let c = coordinates(el, x, y);
+    touch(el, 3000, [c.x0, c.x0], [c.y0, c.y0], null);
+    sendOk(msg.json.command_id);
+  }
+  catch (e) {
+    sendError(e.message, e.code, e.stack, msg.json.command_id);
+  }
+}
+
+/**
+ * Function that performs a double tap
+ */
+function doubleTap(msg) {
+  let command_id = msg.json.command_id;
+  let el;
+  try {
+    el = elementManager.getKnownElement(msg.json.value, curWindow);
+    if (!checkVisible(el, command_id)) {
+      sendError("Element is not currently visible and may not be manipulated", 11, null, command_id);
+      return;
+    }
+    let x = '50%';
+    let y = '50%';
+    let c = coordinates(el, x, y);
+    touch(el, 25, [c.x0, c.x0], [c.y0, c.y0], function() {
+      // When the first tap is done, start a timer for interval ms
+      checkTimer.initWithCallback(function() {
+          //After interval ms, send the second tap
+          touch(el, 25, [c.x0, c.x0], [c.y0, c.y0], null);
+      }, 50, Ci.nsITimer.TYPE_ONE_SHOT);
+    });
+    sendOk(msg.json.command_id);
+  }
+  catch (e) {
+    sendError(e.message, e.code, e.stack, msg.json.command_id);
+  }
+}
+
+/**
  * Function to set the timeout period for element searching 
  */
 function setSearchTimeout(msg) {
   try {
     elementManager.setSearchTimeout(msg.json.value);
   }
   catch (e) {
     sendError(e.message, e.code, e.stack, msg.json.command_id);