Bug 1603074 - part 1: Make synthesizePlainDragAndDrop() synthesize drag events without DataTransfer object r=smaug

`synthesizePlainDragAndDrop()` synthesizes drag events with `DataTransfer`
object which is set to `DragEvent.dataTransfer` of `dragstart` after starting
drag session explicitly.  However, this causes
`EventStateManager::DoDefaltDragStart()` does not initialize `nsIDragService`
instance.  Therefore, synthesized drag events cannot work with editor because
`DragEvent::GetMozSourceNode()` returns `nullptr` due to
`nsIDragSession::GetSourceNode()` returning `nullptr`.

On the other hand, synthesized drag events cannot use
`nsIDragService::InvodeDragSession()` normally because of hitting an assertion.
https://searchfox.org/mozilla-central/rev/690e903ef689a4eca335b96bd903580394864a1c/widget/nsBaseDragService.cpp#230-233

This patch does:
- mark drag events caused by synthesized mouse events as "synthesized for tests"
- make `synthesizePlainDragAndDrop()` stop using
  `nsIDragService.startDragSession()`
- make `nsBaseDragService` initialize and start session even for synthesized
  `dragstart` event
- make `synthesizePlainDragAndDrop()` stop synthesizing drag events with
  `DataTransfer` object since it's normal behavior and it'll be initialized
  with `nsIDragService::GetDataTransfer()`
- make `nsBaseDragService` store `effectAllowed` for the session only when
  it's synthesized session because it's required at initializing synthesized
  default `dropEffect` value of `dragenter`, `dragover`, `dragexit` and `drop`
  events' `dataTransfer`
- make all tests which use `nsIDragService.startDragSession()` use new
  API, `nsIDragService.startDragSessionForTests()` to initialize session's
  `effectAllowed` value
- make `EventStateManager::PostHandleEvent()` set drag end point of the test
  session to `eDrop` event's screen point
- make `synthesizePlainDragAndDrop()` set drag end point of the session if
  it does not synthesize `drop` event because following `endDragSession()`
  use it at dispatching `dragend` event on the source element

Additionally, this adds `dumpFunc` new param to `synthesizePlainDragAndDrop()`
because it's really useful to investigate the reason why requesting DnD isn't
performed as expected.

Differential Revision: https://phabricator.services.mozilla.com/D57425

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Masayuki Nakano 2019-12-21 12:27:06 +00:00
parent 8d04c3f533
commit 87ca855ece
21 changed files with 617 additions and 167 deletions

View File

@ -8,7 +8,11 @@ function simulateItemDragAndEnd(aToDrag, aTarget) {
Ci.nsIDragService
);
ds.startDragSession();
ds.startDragSessionForTests(
Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
Ci.nsIDragService.DRAGDROP_ACTION_COPY |
Ci.nsIDragService.DRAGDROP_ACTION_LINK
);
try {
var [result, dataTransfer] = EventUtils.synthesizeDragOver(
aToDrag.parentNode,

View File

@ -39,7 +39,11 @@ add_task(async function() {
Ci.nsIDragService
);
ds.startDragSession();
ds.startDragSessionForTests(
Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
Ci.nsIDragService.DRAGDROP_ACTION_COPY |
Ci.nsIDragService.DRAGDROP_ACTION_LINK
);
try {
var [result, dataTransfer] = EventUtils.synthesizeDragOver(
identityBox,

View File

@ -1703,13 +1703,15 @@ nsDOMWindowUtils::GetFullZoom(float* aFullZoom) {
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::DispatchDOMEventViaPresShell(nsINode* aTarget, Event* aEvent,
bool* aRetVal) {
NS_IMETHODIMP nsDOMWindowUtils::DispatchDOMEventViaPresShellForTesting(
nsINode* aTarget, Event* aEvent, bool* aRetVal) {
NS_ENSURE_STATE(aEvent);
aEvent->SetTrusted(true);
WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
NS_ENSURE_STATE(internalEvent);
// This API is currently used only by EventUtils.js. Thus we should always
// set mIsSynthesizedForTests to true.
internalEvent->mFlags.mIsSynthesizedForTests = true;
nsCOMPtr<nsIContent> content = do_QueryInterface(aTarget);
NS_ENSURE_STATE(content);
nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(mWindow);

View File

@ -698,9 +698,21 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
KillClickHoldTimer();
}
break;
case eDragOver:
case eDragOver: {
WidgetDragEvent* dragEvent = aEvent->AsDragEvent();
MOZ_ASSERT(dragEvent);
if (dragEvent->mFlags.mIsSynthesizedForTests) {
dragEvent->InitDropEffectForTests();
}
// Send the enter/exit events before eDrop.
GenerateDragDropEnterExit(aPresContext, aEvent->AsDragEvent());
GenerateDragDropEnterExit(aPresContext, dragEvent);
break;
}
case eDrop:
if (aEvent->mFlags.mIsSynthesizedForTests) {
MOZ_ASSERT(aEvent->AsDragEvent());
aEvent->AsDragEvent()->InitDropEffectForTests();
}
break;
case eKeyPress: {
@ -1899,6 +1911,8 @@ void EventStateManager::GenerateDragGesture(nsPresContext* aPresContext,
// get the widget from the target frame
WidgetDragEvent startEvent(aEvent->IsTrusted(), eDragStart, widget);
startEvent.mFlags.mIsSynthesizedForTests =
aEvent->mFlags.mIsSynthesizedForTests;
FillInEventFromGestureDown(&startEvent);
startEvent.mDataTransfer = dataTransfer;
@ -2076,10 +2090,14 @@ bool EventStateManager::DoDefaultDragStart(
// service was called directly within a draggesture handler. In this case,
// don't do anything more, as it is assumed that the handler is managing
// drag and drop manually. Make sure to return true to indicate that a drag
// began.
// began. However, if we're handling drag session for synthesized events,
// we need to initialize some information of the session. Therefore, we
// need to keep going for synthesized case.
nsCOMPtr<nsIDragSession> dragSession;
dragService->GetCurrentSession(getter_AddRefs(dragSession));
if (dragSession) return true;
if (dragSession && !dragSession->IsSynthesizedForTests()) {
return true;
}
// No drag session is currently active, so check if a handler added
// any items to be dragged. If not, there isn't anything to drag.
@ -2098,33 +2116,48 @@ bool EventStateManager::DoDefaultDragStart(
nsCOMPtr<nsIContent> dragTarget = aDataTransfer->GetDragTarget();
if (!dragTarget) {
dragTarget = aDragTarget;
if (!dragTarget) return false;
if (!dragTarget) {
return false;
}
}
// check which drag effect should initially be used. If the effect was not
// set, just use all actions, otherwise Windows won't allow a drop.
uint32_t action = aDataTransfer->EffectAllowedInt();
if (action == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED)
if (action == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) {
action = nsIDragService::DRAGDROP_ACTION_COPY |
nsIDragService::DRAGDROP_ACTION_MOVE |
nsIDragService::DRAGDROP_ACTION_LINK;
}
// get any custom drag image that was set
int32_t imageX, imageY;
RefPtr<Element> dragImage = aDataTransfer->GetDragImage(&imageX, &imageY);
nsCOMPtr<nsIArray> transArray = aDataTransfer->GetTransferables(dragTarget);
if (!transArray) return false;
if (!transArray) {
return false;
}
// After this function returns, the DataTransfer will be cleared so it appears
// empty to content. We need to pass a DataTransfer into the Drag Session, so
// we need to make a copy.
RefPtr<DataTransfer> dataTransfer;
aDataTransfer->Clone(aDragTarget, eDrop, aDataTransfer->MozUserCancelled(),
false, getter_AddRefs(dataTransfer));
if (!dragSession) {
// After this function returns, the DataTransfer will be cleared so it
// appears empty to content. We need to pass a DataTransfer into the Drag
// Session, so we need to make a copy.
aDataTransfer->Clone(aDragTarget, eDrop, aDataTransfer->MozUserCancelled(),
false, getter_AddRefs(dataTransfer));
// Copy over the drop effect, as Clone doesn't copy it for us.
dataTransfer->SetDropEffectInt(aDataTransfer->DropEffectInt());
// Copy over the drop effect, as Clone doesn't copy it for us.
dataTransfer->SetDropEffectInt(aDataTransfer->DropEffectInt());
} else {
MOZ_ASSERT(dragSession->IsSynthesizedForTests());
MOZ_ASSERT(aDragEvent->mFlags.mIsSynthesizedForTests);
// If we're initializing synthesized drag session, we should use given
// DataTransfer as is because it'll be used with following drag events
// in any tests, therefore it should be set to nsIDragSession.dataTransfer
// because it and DragEvent.dataTransfer should be same instance.
dataTransfer = aDataTransfer;
}
// XXXndeakin don't really want to create a new drag DOM event
// here, but we need something to pass to the InvokeDragSession
@ -3613,7 +3646,13 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext,
uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE;
uint32_t action = nsIDragService::DRAGDROP_ACTION_NONE;
if (nsEventStatus_eConsumeNoDefault == *aStatus) {
// if the event has a dataTransfer set, use it.
// If the event has initialized its mDataTransfer, use it.
// Or the event has not been initialized its mDataTransfer, but
// it's set before dispatch because of synthesized, but without
// testing session (e.g., emulating drag from another app), use it
// coming from outside.
// XXX Perhaps, for the latter case, we need new API because we don't
// have a chance to initialize allowed effects of the session.
if (dragEvent->mDataTransfer) {
// get the dataTransfer and the dropEffect that was set on it
dataTransfer = dragEvent->mDataTransfer;
@ -3688,6 +3727,29 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext,
} break;
case eDrop: {
if (aEvent->mFlags.mIsSynthesizedForTests) {
if (nsCOMPtr<nsIDragSession> dragSession =
nsContentUtils::GetDragSession()) {
MOZ_ASSERT(dragSession->IsSynthesizedForTests());
RefPtr<Document> sourceDocument;
DebugOnly<nsresult> rvIgnored =
dragSession->GetSourceDocument(getter_AddRefs(sourceDocument));
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"nsIDragSession::GetSourceDocument() failed, but ignored");
// If source document hasn't been initialized, i.e., dragstart was
// consumed by the test, the test needs to dispatch "dragend" event
// instead of the drag session. Therefore, it does not make sense
// to set drag end point in such case (you hit assersion if you do
// it).
if (sourceDocument) {
CSSIntPoint dropPointInScreen =
Event::GetScreenCoords(aPresContext, aEvent, aEvent->mRefPoint);
dragSession->SetDragEndPointForTests(dropPointInScreen.x,
dropPointInScreen.y);
}
}
}
sLastDragOverFrame = nullptr;
ClearGlobalActiveContent(this);
break;
@ -4755,6 +4817,8 @@ void EventStateManager::GenerateDragDropEnterExit(nsPresContext* aPresContext,
WidgetDragEvent remoteEvent(aDragEvent->IsTrusted(), eDragExit,
aDragEvent->mWidget);
remoteEvent.AssignDragEventData(*aDragEvent, true);
remoteEvent.mFlags.mIsSynthesizedForTests =
aDragEvent->mFlags.mIsSynthesizedForTests;
nsEventStatus remoteStatus = nsEventStatus_eIgnore;
HandleCrossProcessEvent(&remoteEvent, &remoteStatus);
}
@ -4815,6 +4879,8 @@ void EventStateManager::FireDragEnterOrExit(nsPresContext* aPresContext,
nsEventStatus status = nsEventStatus_eIgnore;
WidgetDragEvent event(aDragEvent->IsTrusted(), aMessage, aDragEvent->mWidget);
event.AssignDragEventData(*aDragEvent, false);
event.mFlags.mIsSynthesizedForTests =
aDragEvent->mFlags.mIsSynthesizedForTests;
event.mRelatedTarget = aRelatedTarget;
mCurrentTargetContent = aTargetContent;

View File

@ -7,5 +7,5 @@
bubbles: true
});
let utils = SpecialPowers.getDOMWindowUtils(window);
utils.dispatchDOMEventViaPresShell(document.documentElement, e);
utils.dispatchDOMEventViaPresShellForTesting(document.documentElement, e);
</script>

View File

@ -13,10 +13,10 @@ var gGotNotHandlingDrop = false;
SimpleTest.waitForExplicitFinish();
function fireEvent(target, event) {
SpecialPowers.DOMWindowUtils.dispatchDOMEventViaPresShell(target, event);
SpecialPowers.DOMWindowUtils.dispatchDOMEventViaPresShellForTesting(target, event);
}
function fireDrop(element, shouldAllowDrop, shouldAllowOnlyChromeDrop) {
async function fireDrop(element, shouldAllowDrop, shouldAllowOnlyChromeDrop) {
var ds = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"].
getService(SpecialPowers.Ci.nsIDragService);
@ -31,28 +31,35 @@ function fireDrop(element, shouldAllowDrop, shouldAllowOnlyChromeDrop) {
// need to use real mouse action
window.addEventListener("dragstart", trapDrag, true);
synthesizeMouse(element, 2, 2, { type: "mousedown" });
synthesizeMouse(element, 11, 11, { type: "mousemove" });
synthesizeMouse(element, 20, 20, { type: "mousemove" });
await synthesizePlainDragAndDrop({
srcElement: element,
stepX: 9,
stepY: 9,
expectCancelDragStart: true,
});
window.removeEventListener("dragstart", trapDrag, true);
synthesizeMouse(element, 20, 20, { type: "mouseup" });
ds.startDragSession();
ds.startDragSessionForTests(
SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_COPY |
SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_LINK
); // Session for emulating dnd coming from another app.
try {
var event = document.createEvent("DragEvent");
event.initDragEvent("dragover", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
fireEvent(element, event);
var event = document.createEvent("DragEvent");
event.initDragEvent("dragover", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
fireEvent(element, event);
is(ds.getCurrentSession().canDrop, shouldAllowDrop, "Unexpected .canDrop");
is(ds.getCurrentSession().onlyChromeDrop, shouldAllowOnlyChromeDrop,
"Unexpected .onlyChromeDrop");
is(ds.getCurrentSession().canDrop, shouldAllowDrop, "Unexpected .canDrop");
is(ds.getCurrentSession().onlyChromeDrop, shouldAllowOnlyChromeDrop,
"Unexpected .onlyChromeDrop");
event = document.createEvent("DragEvent");
event.initDragEvent("drop", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
fireEvent(element, event);
ds.endDragSession(false);
ok(!ds.getCurrentSession(), "There shouldn't be a drag session anymore!");
event = document.createEvent("DragEvent");
event.initDragEvent("drop", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer);
fireEvent(element, event);
} finally {
ds.endDragSession(false);
ok(!ds.getCurrentSession(), "There shouldn't be a drag session anymore!");
}
}
var chromeGotEvent = false;
@ -60,10 +67,10 @@ function chromeListener(e) {
chromeGotEvent = true;
}
function runTests()
async function runTests()
{
var targetHandling = document.getElementById("handling_target");
fireDrop(targetHandling, true, false);
await fireDrop(targetHandling, true, false);
is(gGotHandlingDrop, true, "Got drop on accepting element (1)");
is(gGotNotHandlingDrop, false, "Didn't get drop on unaccepting element (1)");
@ -74,7 +81,7 @@ function runTests()
SpecialPowers.addChromeEventListener("drop", chromeListener, true, false);
var targetNotHandling = document.getElementById("nothandling_target");
fireDrop(targetNotHandling, true, true);
await fireDrop(targetNotHandling, true, true);
SpecialPowers.removeChromeEventListener("drop", chromeListener, true);
ok(chromeGotEvent, "Chrome should have got drop event!");
is(gGotHandlingDrop, false, "Didn't get drop on accepting element (2)");

View File

@ -31,7 +31,7 @@ function completeTest(aBox) {
function fireEvent(target, event) {
var win = target.ownerGlobal;
var utils = win.windowUtils;
utils.dispatchDOMEventViaPresShell(target, event);
utils.dispatchDOMEventViaPresShellForTesting(target, event);
}
function RunTest() {

View File

@ -30,7 +30,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=593959
var e = document.createEvent("MouseEvent");
e.initEvent("mousedown", false, false, window, 0, 1, 1, 1, 1,
false, false, false, false, 0, null);
utils.dispatchDOMEventViaPresShell(document.body, e);
utils.dispatchDOMEventViaPresShellForTesting(document.body, e);
is(document.querySelector("body:active"), document.body, "body should be active!")
@ -41,7 +41,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=593959
var e2 = ifrwindow.document.createEvent("MouseEvent");
e2.initEvent("mouseup", false, false, ifrwindow, 0, 1, 1, 1, 1,
false, false, false, false, 0, null);
utils2.dispatchDOMEventViaPresShell(ifrwindow.document.body, e2);
utils2.dispatchDOMEventViaPresShellForTesting(ifrwindow.document.body, e2);
isnot(document.querySelector("body:active"), document.body, "body shouldn't be active!")

View File

@ -951,8 +951,8 @@ interface nsIDOMWindowUtils : nsISupports {
readonly attribute float fullZoom;
/**
* Dispatches aEvent as a trusted event via the PresShell object of the
* window's document.
* Dispatches aEvent as a synthesized trusted event for tests via the
* PresShell object of the window's document.
* The event is dispatched to aTarget, which should be an object
* which implements nsIContent interface (#element, #text, etc).
*
@ -964,8 +964,8 @@ interface nsIDOMWindowUtils : nsISupports {
* Also, aEvent should not be reused.
*/
[can_run_script]
boolean dispatchDOMEventViaPresShell(in Node aTarget,
in Event aEvent);
boolean dispatchDOMEventViaPresShellForTesting(in Node aTarget,
in Event aEvent);
/**
* Sets WidgetEvent::mFlags::mOnlyChromeDispatch to true to ensure that

View File

@ -49,25 +49,30 @@ function sendMouseUp(el) {
function fireEvent(target, event) {
var utils = SpecialPowers.getDOMWindowUtils(window);
utils.dispatchDOMEventViaPresShell(target, event);
utils.dispatchDOMEventViaPresShellForTesting(target, event);
}
function fireDrop(element) {
var ds = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"].
getService(SpecialPowers.Ci.nsIDragService);
ds.startDragSession();
ds.startDragSessionForTests(
SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_COPY |
SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_LINK
); // Session for getting dataTransfer object.
try {
var event = document.createEvent("DragEvent");
event.initDragEvent("dragover", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, null);
fireEvent(element, event);
var event = document.createEvent("DragEvent");
event.initDragEvent("dragover", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, null);
fireEvent(element, event);
event = document.createEvent("DragEvent");
event.initDragEvent("drop", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, null);
fireEvent(element, event);
ds.endDragSession(false);
ok(!ds.getCurrentSession(), "There shouldn't be a drag session anymore!");
event = document.createEvent("DragEvent");
event.initDragEvent("drop", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, null);
fireEvent(element, event);
} finally {
ds.endDragSession(false);
ok(!ds.getCurrentSession(), "There shouldn't be a drag session anymore!");
}
}
function runTest() {

View File

@ -62,19 +62,26 @@ add_task(async function() {
let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
Ci.nsIDragService
);
dragService.startDragSession();
await BrowserTestUtils.synthesizeMouse(
"#target",
5,
15,
{ type: "mousemove" },
browser
dragService.startDragSessionForTests(
Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
Ci.nsIDragService.DRAGDROP_ACTION_COPY |
Ci.nsIDragService.DRAGDROP_ACTION_LINK
);
try {
await BrowserTestUtils.synthesizeMouse(
"#target",
5,
15,
{ type: "mousemove" },
browser
);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 100));
removeEventListener("popupshown", tooltipNotExpected, true);
dragService.endDragSession(true);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 100));
} finally {
removeEventListener("popupshown", tooltipNotExpected, true);
dragService.endDragSession(true);
}
await BrowserTestUtils.synthesizeMouse(
"#target",

View File

@ -318,7 +318,7 @@ function sendDragEvent(aEvent, aTarget, aWindow = window) {
}
var utils = _getDOMWindowUtils(aWindow);
return utils.dispatchDOMEventViaPresShell(aTarget, event);
return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
}
/**
@ -2623,14 +2623,17 @@ function createDragEventObject(
}
// Wrap only in plain mochitests
let dataTransfer = _EU_maybeUnwrap(
_EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType)
);
let dataTransfer;
if (aDataTransfer) {
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;
// 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(
{
@ -2639,7 +2642,7 @@ function createDragEventObject(
screenY: destScreenY,
clientX: destClientX,
clientY: destClientY,
dataTransfer: dataTransfer,
dataTransfer,
_domDispatchOnly: aDragEvent._domDispatchOnly,
},
aDragEvent
@ -2837,7 +2840,24 @@ function synthesizeDrop(
_EU_Ci.nsIDragService
);
ds.startDragSession();
let dropAction;
switch (aDropEffect) {
case null:
case undefined:
case "move":
dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
break;
case "copy":
dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY;
break;
case "link":
dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK;
break;
default:
throw new Error(`${aDropEffect} is an invalid drop effect value`);
}
ds.startDragSessionForTests(dropAction);
try {
var [result, dataTransfer] = synthesizeDragOver(
@ -2866,6 +2886,8 @@ function synthesizeDrop(
* and firing events dragenter, dragover, drop, and dragend.
* This does not modify dataTransfer and tries to emulate the plain drag and
* drop as much as possible, compared to synthesizeDrop.
* Note that if synthesized dragstart is canceled, this throws an exception
* because in such case, Gecko does not start drag session.
*
* @param aParams
* {
@ -2882,6 +2904,9 @@ function synthesizeDrop(
* defaults to the current window object
* destWindow: The window for dispatching event on destElement,
* defaults to the current window object
* expectCancelDragStart: Set to true if the test cancels "dragstart"
* logFunc: Set function which takes one argument if you need
* to log rect of target. E.g., `console.log`.
* }
*/
async function synthesizePlainDragAndDrop(aParams) {
@ -2896,80 +2921,268 @@ async function synthesizePlainDragAndDrop(aParams) {
finalY = srcY + stepY * 2,
srcWindow = window,
destWindow = window,
expectCancelDragStart = false,
logFunc,
} = aParams;
function rectToString(aRect) {
return `left: ${aRect.left}, top: ${aRect.top}, right: ${
aRect.right
}, bottom: ${aRect.bottom}`;
}
if (logFunc) {
logFunc("synthesizePlainDragAndDrop() -- START");
logFunc(
`srcElement.getBoundingClientRect(): ${rectToString(
srcElement.getBoundingClientRect()
)}`
);
}
const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
_EU_Ci.nsIDragService
);
ds.startDragSession();
try {
let dataTransfer = null;
function trapDrag(aEvent) {
dataTransfer = aEvent.dataTransfer;
}
srcElement.addEventListener("dragstart", trapDrag, true);
await new Promise(r => setTimeout(r, 0));
synthesizeMouse(srcElement, srcX, srcY, { type: "mousedown" }, srcWindow);
if (logFunc) {
logFunc(`mousedown at ${srcX}, ${srcY}`);
}
// Wait for the next event tick after each event dispatch, so that UI elements
// (e.g. menu) work like the real user input.
await new Promise(r => setTimeout(r, 0));
let dragStartEvent;
function onDragStart(aEvent) {
dragStartEvent = aEvent;
if (logFunc) {
logFunc(`"${aEvent.type}" event is fired`);
}
if (!srcElement.contains(aEvent.target)) {
// If srcX and srcY does not point in one of rects in srcElement,
// "dragstart" target is not in srcElement. Such case must not
// be expected by this API users so that we should throw an exception
// for making debug easier.
throw new Error(
'event target of "dragstart" is not srcElement nor its descendant'
);
}
}
let dragEnterEvent;
function onDragEnterGenerated(aEvent) {
dragEnterEvent = aEvent;
}
srcWindow.addEventListener("dragstart", onDragStart, { capture: true });
srcWindow.addEventListener("dragenter", onDragEnterGenerated, {
capture: true,
});
try {
// Wait for the next event tick after each event dispatch, so that UI
// elements (e.g. menu) work like the real user input.
await new Promise(r => setTimeout(r, 0));
srcX += stepX;
srcY += stepY;
synthesizeMouse(srcElement, srcX, srcY, { type: "mousemove" }, srcWindow);
await new Promise(r => setTimeout(r, 0));
srcX += stepX;
srcY += stepY;
synthesizeMouse(srcElement, srcX, srcY, { type: "mousemove" }, srcWindow);
await new Promise(r => setTimeout(r, 0));
srcElement.removeEventListener("dragstart", trapDrag, true);
await new Promise(r => setTimeout(r, 0));
let event;
if (destElement) {
// dragover and drop are only fired to a valid drop target. If the
// destElement parameter is null, this function is being used to
// simulate a drag'n'drop over an invalid drop target.
event = createDragEventObject(
"dragover",
destElement,
destWindow,
dataTransfer,
{}
);
sendDragEvent(event, destElement, destWindow);
srcX += stepX;
srcY += stepY;
synthesizeMouse(srcElement, srcX, srcY, { type: "mousemove" }, srcWindow);
if (logFunc) {
logFunc(`first mousemove at ${srcX}, ${srcY}`);
}
await new Promise(r => setTimeout(r, 0));
event = createDragEventObject(
"drop",
destElement,
destWindow,
dataTransfer,
{}
);
sendDragEvent(event, destElement, destWindow);
srcX += stepX;
srcY += stepY;
synthesizeMouse(srcElement, srcX, srcY, { type: "mousemove" }, srcWindow);
if (logFunc) {
logFunc(`second mousemove at ${srcX}, ${srcY}`);
}
await new Promise(r => setTimeout(r, 0));
if (!dragStartEvent) {
throw new Error('"dragstart" event is not fired');
}
} finally {
srcWindow.removeEventListener("dragstart", onDragStart, {
capture: true,
});
srcWindow.removeEventListener("dragenter", onDragEnterGenerated, {
capture: true,
});
}
// dragend is fired, by definition, on the srcElement
event = createDragEventObject(
"dragend",
srcElement,
srcWindow,
dataTransfer,
{ clientX: finalX, clientY: finalY }
);
sendDragEvent(event, srcElement, srcWindow);
let session = ds.getCurrentSession();
if (!session) {
if (expectCancelDragStart) {
synthesizeMouse(srcElement, srcX, srcY, { type: "mouseup" }, srcWindow);
return;
}
throw new Error("drag hasn't been started by the operation");
} else if (expectCancelDragStart) {
throw new Error("drag has been started by the operation");
}
await new Promise(r => setTimeout(r, 0));
if (destElement) {
if (
(srcElement != destElement && !dragEnterEvent) ||
destElement != dragEnterEvent.target
) {
if (logFunc) {
logFunc(
`destElement.getBoundingClientRect(): ${rectToString(
destElement.getBoundingClientRect()
)}`
);
}
function onDragEnter(aEvent) {
dragEnterEvent = aEvent;
if (logFunc) {
logFunc(`"${aEvent.type}" event is fired`);
}
if (aEvent.target != destElement) {
throw new Error('event target of "dragenter" is not destElement');
}
}
destWindow.addEventListener("dragenter", onDragEnter, {
capture: true,
});
try {
let event = createDragEventObject(
"dragenter",
destElement,
destWindow,
null,
{}
);
sendDragEvent(event, destElement, destWindow);
if (!dragEnterEvent && !destElement.disabled) {
throw new Error('"dragenter" event is not fired');
}
if (dragEnterEvent && destElement.disabled) {
throw new Error(
'"dragenter" event should not be fired on disable element'
);
}
} finally {
destWindow.removeEventListener("dragenter", onDragEnter, {
capture: true,
});
}
}
let dragOverEvent;
function onDragOver(aEvent) {
dragOverEvent = aEvent;
if (logFunc) {
logFunc(`"${aEvent.type}" event is fired`);
}
if (aEvent.target != destElement) {
throw new Error('event target of "dragover" is not destElement');
}
}
destWindow.addEventListener("dragover", onDragOver, { capture: true });
try {
// dragover and drop are only fired to a valid drop target. If the
// destElement parameter is null, this function is being used to
// simulate a drag'n'drop over an invalid drop target.
let event = createDragEventObject(
"dragover",
destElement,
destWindow,
null,
{}
);
sendDragEvent(event, destElement, destWindow);
if (!dragOverEvent && !destElement.disabled) {
throw new Error('"dragover" event is not fired');
}
if (dragEnterEvent && destElement.disabled) {
throw new Error(
'"dragover" event should not be fired on disable element'
);
}
} finally {
destWindow.removeEventListener("dragover", onDragOver, {
capture: true,
});
}
await new Promise(r => setTimeout(r, 0));
let dropEvent;
function onDrop(aEvent) {
dropEvent = aEvent;
if (logFunc) {
logFunc(`"${aEvent.type}" event is fired`);
}
if (!destElement.contains(aEvent.target)) {
throw new Error(
'event target of "drop" is not destElement nor its descendant'
);
}
};
destWindow.addEventListener("drop", onDrop, { capture: true });
try {
let event = createDragEventObject(
"drop",
destElement,
destWindow,
null,
{}
);
sendDragEvent(event, destElement, destWindow);
if (!dropEvent && session.canDrop) {
throw new Error('"drop" event is not fired');
}
} finally {
destWindow.removeEventListener("drop", onDrop, { capture: true });
}
} else {
// Since we don't synthesize drop event, we need to set drag end point
// explicitly for "dragEnd" event which will be fired by
// endDragSession().
let event = createDragEventObject(
"dragend",
srcElement,
srcWindow,
null,
{ clientX: finalX, clientY: finalY }
);
session.setDragEndPointForTests(event.screenX, event.screenY);
}
} finally {
ds.endDragSession(true, 0);
await new Promise(r => setTimeout(r, 0));
if (ds.getCurrentSession()) {
let dragEndEvent;
function onDragEnd(aEvent) {
dragEndEvent = aEvent;
if (logFunc) {
logFunc(`"${aEvent.type}" event is fired`);
}
if (!srcElement.contains(aEvent.target)) {
throw new Error(
'event target of "dragend" is not srcElement not its descendant'
);
}
}
srcWindow.addEventListener("dragend", onDragEnd, { capture: true });
try {
ds.endDragSession(true, 0);
if (!dragEndEvent) {
// eslint-disable-next-line no-unsafe-finally
throw new Error(
'"dragend" event is not fired by nsIDragService.endDragSession()'
);
}
} finally {
srcWindow.removeEventListener("dragend", onDragEnd, { capture: true });
}
}
if (logFunc) {
logFunc("synthesizePlainDragAndDrop() -- END");
}
}
}

View File

@ -84,7 +84,7 @@ function dispatchMouseEvent(target, type) {
e.initEvent(type, false, false, win, 0, 1, 1, 1, 1,
false, false, false, false, 0, null);
var utils = SpecialPowers.getDOMWindowUtils(win);
utils.dispatchDOMEventViaPresShell(target, e);
utils.dispatchDOMEventViaPresShellForTesting(target, e);
ok(true, type + " sent to " + target.id);
}

View File

@ -195,7 +195,7 @@ async function dispatchMouseEvent(targetID, type) {
let event = document.createEvent("MouseEvent");
event.initEvent(type, false, false, content, 0, 1, 1, 1, 1,
false, false, false, false, 0, null);
content.windowUtils.dispatchDOMEventViaPresShell(element, event);
content.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event);
/* eslint-enable no-undef */
}
</script>

View File

@ -69,7 +69,7 @@ async function expectLink(browser, expectedLinks, data, testid, onbody=false) {
function dropOnBrowserSync() {
let dropEl = onbody ? browser.contentDocument.body : browser;
synthesizeDrop(dropEl, dropEl, data, "", dropEl.ownerGlobal);
synthesizeDrop(dropEl, dropEl, data, null, dropEl.ownerGlobal);
}
let links;
if (browser.isRemoteBrowser) {

View File

@ -366,6 +366,14 @@ class WidgetDragEvent : public WidgetMouseEvent {
mUserCancelled = false;
mDefaultPreventedOnContent = aEvent.mDefaultPreventedOnContent;
}
/**
* Should be called before dispatching the DOM tree if this event is
* synthesized for tests because drop effect is initialized before
* dispatching from widget if it's not synthesized event, but synthesized
* events are not initialized in the path.
*/
void InitDropEffectForTests();
};
/******************************************************************************

View File

@ -18,6 +18,7 @@
#include "nsCommandParams.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIDragSession.h"
#include "nsPrintfCString.h"
#if defined(XP_WIN)
@ -668,6 +669,53 @@ bool WidgetMouseEvent::IsMiddleClickPasteEnabled() {
return Preferences::GetBool("middlemouse.paste", false);
}
/******************************************************************************
* mozilla::WidgetDragEvent (MouseEvents.h)
******************************************************************************/
void WidgetDragEvent::InitDropEffectForTests() {
MOZ_ASSERT(mFlags.mIsSynthesizedForTests);
nsCOMPtr<nsIDragSession> session = nsContentUtils::GetDragSession();
if (NS_WARN_IF(!session)) {
return;
}
uint32_t effectAllowed = session->GetEffectAllowedForTests();
uint32_t desiredDropEffect = nsIDragService::DRAGDROP_ACTION_NONE;
#ifdef XP_MACOSX
if (IsAlt()) {
desiredDropEffect = IsMeta() ? nsIDragService::DRAGDROP_ACTION_LINK
: nsIDragService::DRAGDROP_ACTION_COPY;
}
#else
// On Linux, we know user's intention from API, but we should use
// same modifiers as Windows for tests because GNOME on Ubuntu use
// them and that makes each test simpler.
if (IsControl()) {
desiredDropEffect = IsShift() ? nsIDragService::DRAGDROP_ACTION_LINK
: nsIDragService::DRAGDROP_ACTION_COPY;
} else if (IsShift()) {
desiredDropEffect = nsIDragService::DRAGDROP_ACTION_MOVE;
}
#endif // #ifdef XP_MACOSX #else
// First, use modifier state for preferring action which is explicitly
// specified by the synthesizer.
if (!(desiredDropEffect &= effectAllowed)) {
// Otherwise, use an action which is allowed at starting the session.
desiredDropEffect = effectAllowed;
}
if (desiredDropEffect & nsIDragService::DRAGDROP_ACTION_MOVE) {
session->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
} else if (desiredDropEffect & nsIDragService::DRAGDROP_ACTION_COPY) {
session->SetDragAction(nsIDragService::DRAGDROP_ACTION_COPY);
} else if (desiredDropEffect & nsIDragService::DRAGDROP_ACTION_LINK) {
session->SetDragAction(nsIDragService::DRAGDROP_ACTION_LINK);
} else {
session->SetDragAction(nsIDragService::DRAGDROP_ACTION_NONE);
}
}
/******************************************************************************
* mozilla::WidgetWheelEvent (MouseEvents.h)
******************************************************************************/

View File

@ -37,6 +37,7 @@
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/DataTransferItemList.h"
#include "mozilla/dom/DataTransfer.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/DragEvent.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/Selection.h"
@ -60,12 +61,14 @@ nsBaseDragService::nsBaseDragService()
: mCanDrop(false),
mOnlyChromeDrop(false),
mDoingDrag(false),
mSessionIsSynthesizedForTests(false),
mEndingSession(false),
mHasImage(false),
mUserCancelled(false),
mDragEventDispatchedToChildProcess(false),
mDragAction(DRAGDROP_ACTION_NONE),
mDragActionFromChildProcess(DRAGDROP_ACTION_UNINITIALIZED),
mEffectAllowedForTests(DRAGDROP_ACTION_UNINITIALIZED),
mContentPolicyType(nsIContentPolicy::TYPE_OTHER),
mSuppressLevel(0),
mInputSource(MouseEvent_Binding::MOZ_SOURCE_MOUSE) {}
@ -208,6 +211,33 @@ void nsBaseDragService::SetDataTransfer(DataTransfer* aDataTransfer) {
mDataTransfer = aDataTransfer;
}
bool nsBaseDragService::IsSynthesizedForTests() {
return mSessionIsSynthesizedForTests;
}
uint32_t nsBaseDragService::GetEffectAllowedForTests() {
MOZ_ASSERT(mSessionIsSynthesizedForTests);
return mEffectAllowedForTests;
}
NS_IMETHODIMP nsBaseDragService::SetDragEndPointForTests(int32_t aScreenX,
int32_t aScreenY) {
MOZ_ASSERT(mDoingDrag);
MOZ_ASSERT(mSourceDocument);
MOZ_ASSERT(mSessionIsSynthesizedForTests);
if (!mDoingDrag || !mSourceDocument || !mSessionIsSynthesizedForTests) {
return NS_ERROR_FAILURE;
}
nsPresContext* presContext = mSourceDocument->GetPresContext();
if (NS_WARN_IF(!presContext)) {
return NS_ERROR_FAILURE;
}
SetDragEndPoint(
LayoutDeviceIntPoint(presContext->CSSPixelsToDevPixels(aScreenX),
presContext->CSSPixelsToDevPixels(aScreenY)));
return NS_OK;
}
//-------------------------------------------------------------------------
NS_IMETHODIMP
nsBaseDragService::InvokeDragSession(
@ -216,23 +246,6 @@ nsBaseDragService::InvokeDragSession(
nsContentPolicyType aContentPolicyType = nsIContentPolicy::TYPE_OTHER) {
AUTO_PROFILER_LABEL("nsBaseDragService::InvokeDragSession", OTHER);
// If you're hitting this, a test is causing the browser to attempt to enter
// the drag-drop native nested event loop, which will put the browser in a
// state that won't run tests properly until there's manual intervention
// to exit the drag-drop loop (either by moving the mouse or hitting escape),
// which can't be done from script since we're in the nested loop.
//
// The best way to avoid this is to catch the dragstart event on the item
// being dragged, and then to call preventDefault() and stopPropagating() on
// it. Alternatively, use EventUtils.synthesizeDragStart, which will do this
// for you.
if (XRE_IsParentProcess()) {
MOZ_ASSERT(
!xpc::IsInAutomation(),
"About to start drag-drop native loop on which will prevent later "
"tests from running properly.");
}
NS_ENSURE_TRUE(aDOMNode, NS_ERROR_INVALID_ARG);
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
@ -250,6 +263,30 @@ nsBaseDragService::InvokeDragSession(
// are in the wrong coord system, so turn off mouse capture.
PresShell::ClearMouseCapture(nullptr);
if (mSessionIsSynthesizedForTests) {
mDoingDrag = true;
mDragAction = aActionType;
mEffectAllowedForTests = aActionType;
return NS_OK;
}
// If you're hitting this, a test is causing the browser to attempt to enter
// the drag-drop native nested event loop, which will put the browser in a
// state that won't run tests properly until there's manual intervention
// to exit the drag-drop loop (either by moving the mouse or hitting escape),
// which can't be done from script since we're in the nested loop.
//
// The best way to avoid this is to catch the dragstart event on the item
// being dragged, and then to call preventDefault() and stopPropagating() on
// it. Alternatively, use EventUtils.synthesizeDragStart, which will do this
// for you.
if (XRE_IsParentProcess()) {
MOZ_ASSERT(
!xpc::IsInAutomation(),
"About to start drag-drop native loop on which will prevent later "
"tests from running properly.");
}
uint32_t length = 0;
mozilla::Unused << aTransferableArray->GetLength(&length);
if (!length) {
@ -299,6 +336,8 @@ nsBaseDragService::InvokeDragSessionWithImage(
NS_ENSURE_TRUE(aDataTransfer, NS_ERROR_NULL_POINTER);
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
mSessionIsSynthesizedForTests =
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
mDataTransfer = aDataTransfer;
mSelection = nullptr;
mHasImage = true;
@ -347,6 +386,8 @@ nsBaseDragService::InvokeDragSessionWithRemoteImage(
NS_ENSURE_TRUE(aDataTransfer, NS_ERROR_NULL_POINTER);
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
mSessionIsSynthesizedForTests =
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
mDataTransfer = aDataTransfer;
mSelection = nullptr;
mHasImage = true;
@ -375,6 +416,8 @@ nsBaseDragService::InvokeDragSessionWithSelection(
NS_ENSURE_TRUE(aDragEvent, NS_ERROR_NULL_POINTER);
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
mSessionIsSynthesizedForTests =
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
mDataTransfer = aDataTransfer;
mSelection = aSelection;
mHasImage = true;
@ -426,6 +469,17 @@ nsBaseDragService::StartDragSession() {
return NS_OK;
}
NS_IMETHODIMP nsBaseDragService::StartDragSessionForTests(
uint32_t aAllowedEffect) {
if (NS_WARN_IF(NS_FAILED(StartDragSession()))) {
return NS_ERROR_FAILURE;
}
mDragAction = aAllowedEffect;
mEffectAllowedForTests = aAllowedEffect;
mSessionIsSynthesizedForTests = true;
return NS_OK;
}
void nsBaseDragService::OpenDragPopup() {
if (mDragPopup) {
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
@ -486,6 +540,8 @@ nsBaseDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
}
mDoingDrag = false;
mSessionIsSynthesizedForTests = false;
mEffectAllowedForTests = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED;
mEndingSession = false;
mCanDrop = false;
@ -542,6 +598,7 @@ nsBaseDragService::FireDragEventAtSource(EventMessage aEventMessage,
if (presShell) {
nsEventStatus status = nsEventStatus_eIgnore;
WidgetDragEvent event(true, aEventMessage, nullptr);
event.mFlags.mIsSynthesizedForTests = mSessionIsSynthesizedForTests;
event.mInputSource = mInputSource;
if (aEventMessage == eDragEnd) {
event.mRefPoint = mEndDragPoint;

View File

@ -148,6 +148,7 @@ class nsBaseDragService : public nsIDragService, public nsIDragSession {
bool mCanDrop;
bool mOnlyChromeDrop;
bool mDoingDrag;
bool mSessionIsSynthesizedForTests;
// true if in EndDragSession
bool mEndingSession;
@ -161,6 +162,10 @@ class nsBaseDragService : public nsIDragService, public nsIDragSession {
uint32_t mDragAction;
uint32_t mDragActionFromChildProcess;
// mEffectAllowedForTests stores allowed effects at invoking the drag
// for tests.
uint32_t mEffectAllowedForTests;
nsCOMPtr<nsINode> mSourceNode;
nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
nsCOMPtr<nsIContentSecurityPolicy> mCsp;

View File

@ -138,7 +138,20 @@ interface nsIDragService : nsISupports
* Tells the Drag Service to start a drag session. This is called when
* an external drag occurs
*/
void startDragSession ( ) ;
void startDragSession() ;
/**
* Similar to startDragSession(), automated tests may want to start
* session for emulating an external drag. At that time, this should
* be used instead of startDragSession().
*
* @param aAllowedEffect Set default drag action which means allowed effects
* in the session and every DnD events are initialized
* with one of specified value. So, the value can be
* DRAGDROP_ACTION_NONE, or one or more values of
* DRAGDROP_ACTION_(MOVE|COPY|LINK).
*/
void startDragSessionForTests(in unsigned long aAllowedEffect);
/**
* Tells the Drag Service to end a drag session. This is called when

View File

@ -105,9 +105,20 @@ interface nsIDragSession : nsISupports
// Change the drag image, using similar arguments as
// nsIDragService::InvokeDragSessionWithImage.
void updateDragImage(in Node aImage, in long aImageX, in long aImageY);
/**
* Returns effects allowed at starting the session for tests.
*/
[notxpcom, nostdcall] unsigned long getEffectAllowedForTests();
/**
* Returns true if current session was started with synthesized drag start.
*/
[notxpcom, nostdcall] bool isSynthesizedForTests();
/**
* Sets drag end point of synthesized session when the test does not dispatch
* "drop" event.
*/
void setDragEndPointForTests(in long aScreenX, in long aScreenY);
};
%{ C++
%}