Bug 1199729 - Part 4: Update EventUtils to simulate drag events more accurately, r=baku
authorMichael Layzell <michael@thelayzells.com>
Wed, 06 Sep 2017 11:36:55 -0400
changeset 429270 5d920dc2ba45a84ab6b3b9fcb5f0a43390bb3c77
parent 429269 aa4ebd8f3dc71221cce47be241e243705d49d4d0
child 429271 83f49da70519de37b7af25f89dcd2883bf4ee441
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1199729
milestone57.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 1199729 - Part 4: Update EventUtils to simulate drag events more accurately, r=baku
dom/events/DataTransfer.cpp
dom/events/DataTransfer.h
dom/webidl/DataTransfer.webidl
testing/mochitest/tests/SimpleTest/EventUtils.js
--- a/dom/events/DataTransfer.cpp
+++ b/dom/events/DataTransfer.cpp
@@ -670,16 +670,35 @@ DataTransfer::PrincipalMaySetData(const 
 }
 
 void
 DataTransfer::TypesListMayHaveChanged()
 {
   DataTransferBinding::ClearCachedTypesValue(this);
 }
 
+already_AddRefed<DataTransfer>
+DataTransfer::MozCloneForEvent(const nsAString& aEvent, ErrorResult& aRv)
+{
+  nsCOMPtr<nsIAtom> atomEvt = NS_Atomize(aEvent);
+  if (!atomEvt) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return nullptr;
+  }
+  EventMessage eventMessage = nsContentUtils::GetEventMessage(atomEvt);
+
+  RefPtr<DataTransfer> dt;
+  nsresult rv = Clone(mParent, eventMessage, false, false, getter_AddRefs(dt));
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+    return nullptr;
+  }
+  return dt.forget();
+}
+
 nsresult
 DataTransfer::SetDataAtInternal(const nsAString& aFormat, nsIVariant* aData,
                                 uint32_t aIndex,
                                 nsIPrincipal* aSubjectPrincipal)
 {
   if (aFormat.IsEmpty()) {
     return NS_OK;
   }
--- a/dom/events/DataTransfer.h
+++ b/dom/events/DataTransfer.h
@@ -310,16 +310,21 @@ public:
                                   nsIVariant* aData,
                                   nsIPrincipal* aPrincipal);
 
   // Notify the DataTransfer that the list returned from GetTypes may have
   // changed.  This can happen due to items we care about for purposes of
   // GetTypes being added or removed or changing item kinds.
   void TypesListMayHaveChanged();
 
+  // Testing method used to emulate internal DataTransfer management.
+  // NOTE: Please don't use this. See the comments in the webidl for more.
+  already_AddRefed<DataTransfer> MozCloneForEvent(const nsAString& aEvent,
+                                                  ErrorResult& aRv);
+
 protected:
 
   // caches text and uri-list data formats that exist in the drag service or
   // clipboard for retrieval later.
   nsresult CacheExternalData(const char* aFormat, uint32_t aIndex,
                              nsIPrincipal* aPrincipal, bool aHidden);
 
   // caches the formats that exist in the drag service that were added by an
--- a/dom/webidl/DataTransfer.webidl
+++ b/dom/webidl/DataTransfer.webidl
@@ -150,9 +150,21 @@ partial interface DataTransfer {
   readonly attribute boolean mozUserCancelled;
 
   /**
    * The node that the mouse was pressed over to begin the drag. For external
    * drags, or if the caller cannot access this node, this will be null.
    */
   [UseCounter]
   readonly attribute Node? mozSourceNode;
+
+  /**
+   * Copy the given DataTransfer for the given event. Used by testing code for
+   * creating emulated Drag and Drop events in the UI.
+   *
+   * NOTE: Don't expose a DataTransfer produced with this method to the web or
+   * use this for non-testing purposes. It can easily be used to get the
+   * DataTransfer into an invalid state, and is an unstable implementation
+   * detail of EventUtils.synthesizeDrag.
+   */
+  [Throws, ChromeOnly]
+  DataTransfer mozCloneForEvent(DOMString event);
 };
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -75,16 +75,26 @@ function _EU_isWin(aWindow = window) {
   if (aWindow) {
     try {
       return aWindow.navigator.platform.indexOf("Win") > -1;
     } catch (ex) {}
   }
   return navigator.platform.indexOf("Win") > -1;
 }
 
+function _EU_maybeWrap(o) {
+  var c = Object.getOwnPropertyDescriptor(window, 'Components');
+  return c.value && !c.writable ? o : SpecialPowers.wrap(o);
+}
+
+function _EU_maybeUnwrap(o) {
+  var c = Object.getOwnPropertyDescriptor(window, 'Components');
+  return c.value && !c.writable ? o : SpecialPowers.unwrap(o);
+}
+
 /**
  * Send a mouse event to the node aTarget (aTarget can be an id, or an
  * actual node) . The "event" passed in to aEvent is just a JavaScript
  * object with the properties set that the real mouse event object should
  * have. This includes the type of the mouse event.
  * E.g. to send an click event to the node with id 'node' you might do this:
  *
  * sendMouseEvent({type:'click'}, 'node');
@@ -2102,20 +2112,31 @@ function createDragEventObject(aType, aD
   var destScreenX = aDestWindow.mozInnerScreenX + destClientX;
   var destScreenY = aDestWindow.mozInnerScreenY + destClientY;
   if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) {
     aDragEvent.screenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX;
   }
   if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) {
     aDragEvent.screenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY;
   }
-  return Object.assign({ type: aType,
-                         screenX: destScreenX, screenY: destScreenY,
-                         clientX: destClientX, clientY: destClientY,
-                         dataTransfer: aDataTransfer }, aDragEvent);
+
+  // Wrap only in plain mochitests
+  let dataTransfer = _EU_maybeUnwrap(_EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType));
+
+  // Copy over the drop effect. This isn't copied over by Clone, as it uses more
+  // complex logic in the actual implementation (see
+  // nsContentUtils::SetDataTransferInEvent for actual impl).
+  dataTransfer.dropEffect = aDataTransfer.dropEffect;
+
+  return Object.assign({
+    type: aType,
+    screenX: destScreenX, screenY: destScreenY,
+    clientX: destClientX, clientY: destClientY,
+    dataTransfer: dataTransfer,
+  }, aDragEvent);
 }
 
 /**
  * Emulate a event sequence of dragstart, dragenter, and dragover.
  *
  * @param aSrcElement   The element to use to start the drag.
  * @param aDestElement  The element to fire the dragover, dragenter events
  * @param aDragData     The data to supply for the data transfer.
@@ -2139,48 +2160,60 @@ function synthesizeDragOver(aSrcElement,
 {
   if (!aWindow) {
     aWindow = window;
   }
   if (!aDestWindow) {
     aDestWindow = aWindow;
   }
 
-  var dataTransfer;
-  var trapDrag = function(event) {
-    dataTransfer = event.dataTransfer;
+  const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService(_EU_Ci.nsIObserverService);
+  const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(_EU_Ci.nsIDragService);
+  var sess = ds.getCurrentSession();
+
+  // This method runs before other callbacks, and acts as a way to inject the
+  // initial drag data into the DataTransfer.
+  function fillDrag(event) {
     if (aDragData) {
       for (var i = 0; i < aDragData.length; i++) {
         var item = aDragData[i];
         for (var j = 0; j < item.length; j++) {
-          dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
+          event.dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
         }
       }
     }
-    dataTransfer.dropEffect = aDropEffect || "move";
+    event.dataTransfer.dropEffect = aDropEffect || "move";
     event.preventDefault();
-  };
+  }
+
+  function trapDrag(subject, topic) {
+    if (topic == "on-datatransfer-available") {
+      sess.dataTransfer = _EU_maybeUnwrap(_EU_maybeWrap(subject).mozCloneForEvent("drop"));
+      sess.dataTransfer.dropEffect = subject.dropEffect;
+    }
+  }
 
   // need to use real mouse action
-  aWindow.addEventListener("dragstart", trapDrag, true);
+  aWindow.addEventListener("dragstart", fillDrag, true);
+  obs.addObserver(trapDrag, "on-datatransfer-available");
   synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow);
 
   var rect = aSrcElement.getBoundingClientRect();
   var x = rect.width / 2;
   var y = rect.height / 2;
   synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow);
   synthesizeMouse(aSrcElement, x+10, y+10, { type: "mousemove" }, aWindow);
-  aWindow.removeEventListener("dragstart", trapDrag, true);
+  aWindow.removeEventListener("dragstart", fillDrag, true);
+  obs.removeObserver(trapDrag, "on-datatransfer-available");
 
-  var event = createDragEventObject("dragenter", aDestElement, aDestWindow,
+  var dataTransfer = sess.dataTransfer;
+
+  // The EventStateManager will fire our dragenter event if it needs to.
+  var event = createDragEventObject("dragover", aDestElement, aDestWindow,
                                     dataTransfer, aDragEvent);
-  sendDragEvent(event, aDestElement, aDestWindow);
-
-  event = createDragEventObject("dragover", aDestElement, aDestWindow,
-                                dataTransfer, aDragEvent);
   var result = sendDragEvent(event, aDestElement, aDestWindow);
 
   return [result, dataTransfer];
 }
 
 /**
  * Emulate the drop event and mouseup event.
  * This should be called after synthesizeDragOver.