Bug 1886604: Part 1: Add JS-scriptable MockDragService r=win-reviewers,edgar,rkraesig

Introduces MockDragService as a subclass/implementation of nsBaseDragService.
This class is based on the Windows implementation of nsBaseDragService.  The
service is created with a MockDragServiceController that JS can use to
initiate drag events that would normally come from the system.  Native
drag-and-drop is not permitted in automation because it puts the browser into
a state where, if anything goes wrong (e.g. a test failure), it cannot be
killed without manual intervention.  This allows us to avoid that limitation
while still testing most of the browser's drag-and-drop behavior, including
the (substantial and complex) nsBaseDragService base-class that is common to
all platforms.

Differential Revision: https://phabricator.services.mozilla.com/D205640
This commit is contained in:
David P 2024-07-04 01:03:33 +00:00
parent e247d7e4d1
commit 7d6e26c06c
8 changed files with 418 additions and 19 deletions

View File

@ -983,6 +983,13 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
mMouseEnterLeaveHelper->TryToRestorePendingRemovedOverTarget(aEvent);
}
static constexpr auto const allowSynthesisForTests = []() -> bool {
nsCOMPtr<nsIDragService> dragService =
do_GetService("@mozilla.org/widget/dragservice;1");
return dragService &&
!dragService->GetNeverAllowSessionIsSynthesizedForTests();
};
switch (aEvent->mMessage) {
case eContextMenu:
if (PointerLockManager::IsLocked()) {
@ -1142,20 +1149,21 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
case eDragOver: {
WidgetDragEvent* dragEvent = aEvent->AsDragEvent();
MOZ_ASSERT(dragEvent);
if (dragEvent->mFlags.mIsSynthesizedForTests) {
if (dragEvent->mFlags.mIsSynthesizedForTests &&
allowSynthesisForTests()) {
dragEvent->InitDropEffectForTests();
}
// Send the enter/exit events before eDrop.
GenerateDragDropEnterExit(aPresContext, dragEvent);
break;
}
case eDrop:
if (aEvent->mFlags.mIsSynthesizedForTests) {
case eDrop: {
if (aEvent->mFlags.mIsSynthesizedForTests && allowSynthesisForTests()) {
MOZ_ASSERT(aEvent->AsDragEvent());
aEvent->AsDragEvent()->InitDropEffectForTests();
}
break;
}
case eKeyPress: {
WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent();
if (keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eChrome) ||
@ -4347,8 +4355,11 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext,
case eDrop: {
if (aEvent->mFlags.mIsSynthesizedForTests) {
if (nsCOMPtr<nsIDragSession> dragSession =
nsContentUtils::GetDragSession()) {
nsCOMPtr<nsIDragService> dragService =
do_GetService("@mozilla.org/widget/dragservice;1");
nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession();
if (dragSession && dragService &&
!dragService->GetNeverAllowSessionIsSynthesizedForTests()) {
MOZ_ASSERT(dragSession->IsSynthesizedForTests());
RefPtr<WindowContext> sourceWC;
DebugOnly<nsresult> rvIgnored =

View File

@ -0,0 +1,204 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "MockDragServiceController.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DragEvent.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "nsIFrame.h"
#include "nsPresContext.h"
#include "nsBaseDragService.h"
namespace mozilla::test {
NS_IMPL_ISUPPORTS(MockDragServiceController, nsIMockDragServiceController)
class MockDragService : public nsBaseDragService {
public:
MOZ_CAN_RUN_SCRIPT nsresult
InvokeDragSessionImpl(nsIArray* aTransferableArray,
const mozilla::Maybe<mozilla::CSSIntRegion>& aRegion,
uint32_t aActionType) override {
// In Windows' nsDragService, InvokeDragSessionImpl would establish a
// nested (native) event loop that runs as long as the drag is happening.
// See DoDragDrop in MSDN.
// We cannot do anything like that here since it would block mochitest
// from scripting behavior with MockDragServiceController::SendDragEvent.
// mDragAction is not yet handled properly in the MockDragService.
// This should be updated with each drag event. Instead, we always MOVE.
mDragAction = DRAGDROP_ACTION_MOVE;
StartDragSession();
return NS_OK;
}
bool IsMockService() override { return true; }
uint32_t mLastModifierKeyState = 0;
};
static void SetDragEndPointFromScreenPoint(
MockDragService* aService, nsPresContext* aPc,
const LayoutDeviceIntPoint& aScreenPt) {
// aScreenPt is screen-relative, and we want to be
// top-level-widget-relative.
auto* widget = aPc->GetRootWidget();
auto pt = aScreenPt - widget->WidgetToScreenOffset();
pt += widget->WidgetToTopLevelWidgetOffset();
aService->SetDragEndPoint(pt);
}
static bool IsMouseEvent(nsIMockDragServiceController::EventType aEventType) {
using EventType = nsIMockDragServiceController::EventType;
switch (aEventType) {
case EventType::eMouseDown:
case EventType::eMouseMove:
case EventType::eMouseUp:
return true;
default:
return false;
}
}
static EventMessage MockEventTypeToEventMessage(
nsIMockDragServiceController::EventType aEventType) {
using EventType = nsIMockDragServiceController::EventType;
switch (aEventType) {
case EventType::eDragEnter:
return EventMessage::eDragEnter;
case EventType::eDragOver:
return EventMessage::eDragOver;
case EventType::eDragLeave:
return EventMessage::eDragLeave;
case EventType::eDrop:
return EventMessage::eDrop;
case EventType::eMouseDown:
return EventMessage::eMouseDown;
case EventType::eMouseMove:
return EventMessage::eMouseMove;
case EventType::eMouseUp:
return EventMessage::eMouseUp;
default:
MOZ_ASSERT(false, "Invalid event type");
return EventMessage::eVoidEvent;
}
}
MockDragServiceController::MockDragServiceController()
: mDragService(new MockDragService()) {}
MockDragServiceController::~MockDragServiceController() = default;
NS_IMETHODIMP
MockDragServiceController::GetMockDragService(nsIDragService** aService) {
RefPtr<nsIDragService> ds = mDragService;
ds.forget(aService);
return NS_OK;
}
NS_IMETHODIMP
MockDragServiceController::SendEvent(
dom::BrowsingContext* aBC,
nsIMockDragServiceController::EventType aEventType, int32_t aScreenX,
int32_t aScreenY, uint32_t aKeyModifiers = 0) {
RefPtr<nsIWidget> widget =
aBC->Canonical()->GetParentProcessWidgetContaining();
NS_ENSURE_TRUE(widget, NS_ERROR_UNEXPECTED);
auto* embedder = aBC->Top()->GetEmbedderElement();
NS_ENSURE_TRUE(embedder, NS_ERROR_UNEXPECTED);
auto* frame = embedder->GetPrimaryFrame();
NS_ENSURE_TRUE(frame, NS_ERROR_UNEXPECTED);
auto* presCxt = frame->PresContext();
MOZ_ASSERT(presCxt);
EventMessage eventType = MockEventTypeToEventMessage(aEventType);
UniquePtr<WidgetInputEvent> widgetEvent;
if (IsMouseEvent(aEventType)) {
widgetEvent = MakeUnique<WidgetMouseEvent>(true, eventType, widget,
WidgetMouseEvent::Reason::eReal);
} else {
widgetEvent = MakeUnique<WidgetDragEvent>(true, eventType, widget);
}
widgetEvent->mWidget = widget;
widgetEvent->mFlags.mIsSynthesizedForTests = true;
auto clientPosInScreenCoords = widget->GetClientBounds().TopLeft();
widgetEvent->mRefPoint =
LayoutDeviceIntPoint(aScreenX, aScreenY) - clientPosInScreenCoords;
RefPtr<MockDragService> ds = mDragService;
ds->mLastModifierKeyState = aKeyModifiers;
if (aEventType == EventType::eDragEnter) {
// We expect StartDragSession to return an "error" when a drag session
// already exists, which it will since we are testing dragging from
// inside Gecko, so we don't check the return value.
ds->StartDragSession();
}
nsCOMPtr<nsIDragSession> currentDragSession;
nsresult rv = ds->GetCurrentSession(getter_AddRefs(currentDragSession));
NS_ENSURE_SUCCESS(rv, rv);
switch (aEventType) {
case EventType::eMouseDown:
case EventType::eMouseMove:
case EventType::eMouseUp:
widget->DispatchInputEvent(widgetEvent.get());
break;
case EventType::eDragEnter:
NS_ENSURE_TRUE(currentDragSession, NS_ERROR_UNEXPECTED);
currentDragSession->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
widget->DispatchInputEvent(widgetEvent.get());
break;
case EventType::eDragLeave: {
NS_ENSURE_TRUE(currentDragSession, NS_ERROR_UNEXPECTED);
currentDragSession->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
widget->DispatchInputEvent(widgetEvent.get());
SetDragEndPointFromScreenPoint(ds, presCxt,
LayoutDeviceIntPoint(aScreenX, aScreenY));
nsCOMPtr<nsINode> sourceNode;
rv = currentDragSession->GetSourceNode(getter_AddRefs(sourceNode));
NS_ENSURE_SUCCESS(rv, rv);
if (!sourceNode) {
rv = ds->EndDragSession(false /* doneDrag */, aKeyModifiers);
NS_ENSURE_SUCCESS(rv, rv);
}
} break;
case EventType::eDragOver:
NS_ENSURE_TRUE(currentDragSession, NS_ERROR_UNEXPECTED);
rv = ds->FireDragEventAtSource(EventMessage::eDrag, aKeyModifiers);
currentDragSession->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
NS_ENSURE_SUCCESS(rv, rv);
widget->DispatchInputEvent(widgetEvent.get());
break;
case EventType::eDrop: {
NS_ENSURE_TRUE(currentDragSession, NS_ERROR_UNEXPECTED);
currentDragSession->SetDragAction(nsIDragService::DRAGDROP_ACTION_MOVE);
widget->DispatchInputEvent(widgetEvent.get());
SetDragEndPointFromScreenPoint(ds, presCxt,
LayoutDeviceIntPoint(aScreenX, aScreenY));
rv = ds->EndDragSession(true /* doneDrag */, aKeyModifiers);
NS_ENSURE_SUCCESS(rv, rv);
} break;
default:
MOZ_ASSERT_UNREACHABLE("Impossible event type?");
return NS_ERROR_FAILURE;
}
return NS_OK;
}
NS_IMETHODIMP
MockDragServiceController::CancelDrag(uint32_t aKeyModifiers = 0) {
RefPtr<MockDragService> ds = mDragService;
ds->mLastModifierKeyState = aKeyModifiers;
return ds->EndDragSession(false /* doneDrag */, aKeyModifiers);
}
} // namespace mozilla::test

View File

@ -0,0 +1,29 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#ifndef MockDragServiceController_h__
#define MockDragServiceController_h__
#include "nsIMockDragServiceController.h"
namespace mozilla::test {
class MockDragService;
class MockDragServiceController : public nsIMockDragServiceController {
public:
MockDragServiceController();
NS_DECL_ISUPPORTS
NS_DECL_NSIMOCKDRAGSERVICECONTROLLER
private:
virtual ~MockDragServiceController();
RefPtr<MockDragService> mDragService;
};
} // namespace mozilla::test
#endif // MockDragServiceController_h__

View File

@ -124,6 +124,7 @@ XPIDL_SOURCES += [
"nsIFormatConverter.idl",
"nsIGfxInfo.idl",
"nsIGfxInfoDebug.idl",
"nsIMockDragServiceController.idl",
"nsIPaper.idl",
"nsIPaperMargin.idl",
"nsIPrinter.idl",
@ -383,6 +384,12 @@ IPDL_SOURCES += [
LOCAL_INCLUDES += [
"/widget/%s" % toolkit,
]
if CONFIG["ENABLE_TESTS"]:
UNIFIED_SOURCES += [
"MockDragServiceController.cpp",
]
FINAL_LIBRARY = "xul"
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":

View File

@ -50,6 +50,8 @@
#include "gfxContext.h"
#include "gfxPlatform.h"
#include "nscore.h"
#include "MockDragServiceController.h"
#include <algorithm>
using namespace mozilla;
@ -341,18 +343,17 @@ nsBaseDragService::InvokeDragSession(
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.
if (XRE_IsParentProcess()) {
// 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 use the mock service in tests. See
// synthesizeMockDragAndDrop.
MOZ_ASSERT(
!xpc::IsInAutomation(),
!xpc::IsInAutomation() || IsMockService(),
"About to start drag-drop native loop on which will prevent later "
"tests from running properly.");
}
@ -409,7 +410,8 @@ nsBaseDragService::InvokeDragSessionWithImage(
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
mSessionIsSynthesizedForTests =
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests &&
!GetNeverAllowSessionIsSynthesizedForTests();
mDataTransfer = aDataTransfer;
mSelection = nullptr;
mHasImage = true;
@ -460,7 +462,8 @@ nsBaseDragService::InvokeDragSessionWithRemoteImage(
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
mSessionIsSynthesizedForTests =
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests &&
!GetNeverAllowSessionIsSynthesizedForTests();
mDataTransfer = aDataTransfer;
mSelection = nullptr;
mHasImage = true;
@ -492,7 +495,8 @@ nsBaseDragService::InvokeDragSessionWithSelection(
NS_ENSURE_TRUE(mSuppressLevel == 0, NS_ERROR_FAILURE);
mSessionIsSynthesizedForTests =
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests;
aDragEvent->WidgetEventPtr()->mFlags.mIsSynthesizedForTests &&
!GetNeverAllowSessionIsSynthesizedForTests();
mDataTransfer = aDataTransfer;
mSelection = aSelection;
mHasImage = true;
@ -550,6 +554,8 @@ nsBaseDragService::StartDragSession() {
NS_IMETHODIMP nsBaseDragService::StartDragSessionForTests(
uint32_t aAllowedEffect) {
// This method must set mSessionIsSynthesizedForTests
MOZ_ASSERT(!mNeverAllowSessionIsSynthesizedForTests);
if (NS_WARN_IF(NS_FAILED(StartDragSession()))) {
return NS_ERROR_FAILURE;
}
@ -1042,3 +1048,38 @@ nsBaseDragService::MaybeEditorDeletedSourceNode(Element* aEditingHost) {
}
return NS_OK;
}
NS_IMETHODIMP
nsBaseDragService::GetMockDragController(
nsIMockDragServiceController** aController) {
#ifdef ENABLE_TESTS
if (XRE_IsContentProcess()) {
// The mock drag controller is only available in the parent process.
MOZ_ASSERT(!XRE_IsContentProcess());
return NS_ERROR_NOT_AVAILABLE;
}
if (!mMockController) {
mMockController = new mozilla::test::MockDragServiceController();
}
auto controller = mMockController;
controller.forget(aController);
return NS_OK;
#else
*aController = nullptr;
MOZ_ASSERT(false, "CreateMockDragController may only be called for testing");
return NS_ERROR_NOT_AVAILABLE;
#endif
}
NS_IMETHODIMP
nsBaseDragService::GetNeverAllowSessionIsSynthesizedForTests(
bool* aNeverAllow) {
*aNeverAllow = mNeverAllowSessionIsSynthesizedForTests;
return NS_OK;
}
NS_IMETHODIMP
nsBaseDragService::SetNeverAllowSessionIsSynthesizedForTests(bool aNeverAllow) {
mNeverAllowSessionIsSynthesizedForTests = aNeverAllow;
return NS_OK;
}

View File

@ -45,6 +45,10 @@ namespace dom {
class DataTransfer;
class Selection;
} // namespace dom
namespace test {
class MockDragServiceController;
} // namespace test
} // namespace mozilla
/**
@ -146,10 +150,13 @@ class nsBaseDragService : public nsIDragService, public nsIDragSession {
return retval;
}
virtual bool IsMockService() { return false; }
bool mCanDrop;
bool mOnlyChromeDrop;
bool mDoingDrag;
bool mSessionIsSynthesizedForTests;
bool mIsDraggingTextInTextControl;
// true if in EndDragSession
@ -216,6 +223,13 @@ class nsBaseDragService : public nsIDragService, public nsIDragSession {
// Sub-region for tree-selections.
mozilla::Maybe<mozilla::CSSIntRegion> mRegion;
RefPtr<mozilla::test::MockDragServiceController> mMockController;
// If this is set, mSessionIsSynthesizedForTests should not become true.
// This hack is used to bypass the "old" drag-drop test behavior.
// See nsIDragService.idl for details.
bool mNeverAllowSessionIsSynthesizedForTests = false;
};
#endif // nsBaseDragService_h__

View File

@ -15,6 +15,7 @@ webidl Node;
webidl Selection;
interface nsICookieJarSettings;
interface nsIMockDragServiceController;
%{C++
#include "mozilla/EventForwards.h"
@ -205,6 +206,30 @@ interface nsIDragService : nsISupports
* @param aEditingHost The editing host when the editor deletes selection.
*/
[noscript] void maybeEditorDeletedSourceNode(in Element aEditingHost);
/**
* The controller is used to issue events that would normally come from
* the system (when it is not mocked for testing). This allows us to test
* without engaging any native DND behavior.
*
* In order for the controller to be effective, the existing nsIDragService
* needs to be replaced with the one in the controller. See
* nsIMockDragServiceController for details.
*/
nsIMockDragServiceController getMockDragController();
/**
* If this is true, mSessionIsSynthesizedForTests should not become true.
* This hack is used to bypass the "old" drag-drop test behavior, which needed
* special behavior to pass. The new framework requires the actual browser
* behavior. The test for whether we are using the new framework or not can
* only be done in the main process with nsBaseDragService::IsMockService.
* Unfortunately, mSessionIsSynthesizedForTests is inherited from
* synthetic mouse events in content processes (when dragging content) so we
* set this early in the relevant tests. Once the old tests are replaced,
* this can be removed along with mSessionIsSynthesizedForTests.
*/
[infallible] attribute boolean neverAllowSessionIsSynthesizedForTests;
};

View File

@ -0,0 +1,68 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
*
* 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/. */
#include "nsISupports.idl"
webidl BrowsingContext;
interface nsIDragService;
/**
* A driver for MockDragService, so that tests can mock system DND behavior.
* (System DND is not permitted in tests.)
*/
[scriptable, builtinclass, uuid(32037ab0-bfc7-11ee-9f4b-09901bed55fa)]
interface nsIMockDragServiceController : nsISupports
{
// Types of event that can be sent by this controller.
cenum EventType : 8 {
eDragEnter = 0,
eDragOver = 1,
eDragLeave = 2,
eDrop = 3,
eMouseDown = 4,
eMouseMove = 5,
eMouseUp = 6,
};
/**
* The nsIDragService that this controller targets. It is a mock version
* of the normal nsIDragService. The caller must replace the drag
* service in the service manager with this object before sending
* drag events to it. This can be done with MockRegistrar or by calling
* the nsComponentManager directly.
*/
readonly attribute nsIDragService mockDragService;
/**
* Issue the given event from our mock drag service, as if that type
* of event came from the system. The mock object attempts to mimic the
* essential behavior of the native drag classes for this.
*
* @param aBC A BrowsingContext in the widget the event is
* targetted at
* @param aEventType Type of event to send
* @param aScreenX Screen X coordinate of event
* @param aScreenY Screen Y coordinate of event
* @param aKeyModifiers Keys that are pressed during event.
* NOTE: Keys should be interpreted as selecting
* the drag action, but that logic is very
* platform-dependent and is not yet mocked.
* Drops will be processed as "moves".
*/
[can_run_script]
void sendEvent(in BrowsingContext aBC,
in nsIMockDragServiceController_EventType aEventType,
in long aScreenX, in long aScreenY,
[optional] in uint32_t aKeyModifiers);
/**
* Windows' IDropTarget has the ability to "Cancel" a drag that is
* different than dragleave. This emulates that behavior for testing.
*/
[can_run_script]
void cancelDrag([optional] in uint32_t aKeyModifiers);
};