diff options
Diffstat (limited to 'gfx/layers/apz/test/gtest/APZTestCommon.h')
-rw-r--r-- | gfx/layers/apz/test/gtest/APZTestCommon.h | 1094 |
1 files changed, 1094 insertions, 0 deletions
diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.h b/gfx/layers/apz/test/gtest/APZTestCommon.h new file mode 100644 index 0000000000..414f9c7377 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestCommon.h @@ -0,0 +1,1094 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_layers_APZTestCommon_h +#define mozilla_layers_APZTestCommon_h + +/** + * Defines a set of mock classes and utility functions/classes for + * writing APZ gtests. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "mozilla/Attributes.h" +#include "mozilla/layers/GeckoContentController.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/DoubleTapToZoom.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/MatrixMessage.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" +#include "apz/src/APZCTreeManager.h" +#include "apz/src/AsyncPanZoomController.h" +#include "apz/src/HitTestingTreeNode.h" +#include "base/task.h" +#include "gfxPlatform.h" +#include "TestWRScrollData.h" +#include "UnitTransforms.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layers; +using ::testing::_; +using ::testing::AtLeast; +using ::testing::AtMost; +using ::testing::InSequence; +using ::testing::MockFunction; +using ::testing::NiceMock; +typedef mozilla::layers::GeckoContentController::TapType TapType; + +inline TimeStamp GetStartupTime() { + static TimeStamp sStartupTime = TimeStamp::Now(); + return sStartupTime; +} + +inline uint32_t MillisecondsSinceStartup(TimeStamp aTime) { + return (aTime - GetStartupTime()).ToMilliseconds(); +} + +// Some helper functions for constructing input event objects suitable to be +// passed either to an APZC (which expects an transformed point), or to an APZTM +// (which expects an untransformed point). We handle both cases by setting both +// the transformed and untransformed fields to the same value. +inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier, + const ScreenIntPoint& aPoint) { + SingleTouchData touch(aIdentifier, aPoint, ScreenSize(0, 0), 0, 0); + touch.mLocalScreenPoint = ParentLayerPoint(aPoint.x, aPoint.y); + return touch; +} + +// Convenience wrapper for CreateSingleTouchData() that takes loose coordinates. +inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier, + ScreenIntCoord aX, + ScreenIntCoord aY) { + return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY)); +} + +inline PinchGestureInput CreatePinchGestureInput( + PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus, + float aCurrentSpan, float aPreviousSpan, TimeStamp timestamp) { + ParentLayerPoint localFocus(aFocus.x, aFocus.y); + PinchGestureInput result(aType, PinchGestureInput::UNKNOWN, timestamp, + ExternalPoint(0, 0), aFocus, aCurrentSpan, + aPreviousSpan, 0); + return result; +} + +template <class SetArg, class Storage> +class ScopedGfxSetting { + public: + ScopedGfxSetting(const std::function<SetArg(void)>& aGetPrefFunc, + const std::function<void(SetArg)>& aSetPrefFunc, SetArg aVal) + : mSetPrefFunc(aSetPrefFunc) { + mOldVal = aGetPrefFunc(); + aSetPrefFunc(aVal); + } + + ~ScopedGfxSetting() { mSetPrefFunc(mOldVal); } + + private: + std::function<void(SetArg)> mSetPrefFunc; + Storage mOldVal; +}; + +static inline constexpr auto kDefaultTouchBehavior = + AllowedTouchBehavior::VERTICAL_PAN | AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | AllowedTouchBehavior::ANIMATING_ZOOM; + +#define FRESH_PREF_VAR_PASTE(id, line) id##line +#define FRESH_PREF_VAR_EXPAND(id, line) FRESH_PREF_VAR_PASTE(id, line) +#define FRESH_PREF_VAR FRESH_PREF_VAR_EXPAND(pref, __LINE__) + +#define SCOPED_GFX_PREF_BOOL(prefName, prefValue) \ + ScopedGfxSetting<bool, bool> FRESH_PREF_VAR( \ + [=]() { return Preferences::GetBool(prefName); }, \ + [=](bool aPrefValue) { Preferences::SetBool(prefName, aPrefValue); }, \ + prefValue) + +#define SCOPED_GFX_PREF_INT(prefName, prefValue) \ + ScopedGfxSetting<int32_t, int32_t> FRESH_PREF_VAR( \ + [=]() { return Preferences::GetInt(prefName); }, \ + [=](int32_t aPrefValue) { Preferences::SetInt(prefName, aPrefValue); }, \ + prefValue) + +#define SCOPED_GFX_PREF_FLOAT(prefName, prefValue) \ + ScopedGfxSetting<float, float> FRESH_PREF_VAR( \ + [=]() { return Preferences::GetFloat(prefName); }, \ + [=](float aPrefValue) { Preferences::SetFloat(prefName, aPrefValue); }, \ + prefValue) + +class MockContentController : public GeckoContentController { + public: + MOCK_METHOD1(NotifyLayerTransforms, void(nsTArray<MatrixMessage>&&)); + MOCK_METHOD1(RequestContentRepaint, void(const RepaintRequest&)); + MOCK_METHOD6(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers, + const ScrollableLayerGuid&, uint64_t, + const Maybe<DoubleTapToZoomMetrics>&)); + MOCK_METHOD5(NotifyPinchGesture, + void(PinchGestureInput::PinchGestureType, + const ScrollableLayerGuid&, const LayoutDevicePoint&, + LayoutDeviceCoord, Modifiers)); + // Can't use the macros with already_AddRefed :( + void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) { + RefPtr<Runnable> task = aTask; + } + bool IsRepaintThread() { return NS_IsMainThread(); } + void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) { + NS_DispatchToMainThread(std::move(aTask)); + } + MOCK_METHOD4(NotifyAPZStateChange, + void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, + int aArg, Maybe<uint64_t> aInputBlockId)); + MOCK_METHOD0(NotifyFlushComplete, void()); + MOCK_METHOD3(NotifyAsyncScrollbarDragInitiated, + void(uint64_t, const ScrollableLayerGuid::ViewID&, + ScrollDirection aDirection)); + MOCK_METHOD1(NotifyAsyncScrollbarDragRejected, + void(const ScrollableLayerGuid::ViewID&)); + MOCK_METHOD1(NotifyAsyncAutoscrollRejected, + void(const ScrollableLayerGuid::ViewID&)); + MOCK_METHOD1(CancelAutoscroll, void(const ScrollableLayerGuid&)); + MOCK_METHOD2(NotifyScaleGestureComplete, + void(const ScrollableLayerGuid&, float aScale)); + MOCK_METHOD4(UpdateOverscrollVelocity, + void(const ScrollableLayerGuid&, float, float, bool)); + MOCK_METHOD4(UpdateOverscrollOffset, + void(const ScrollableLayerGuid&, float, float, bool)); +}; + +class MockContentControllerDelayed : public MockContentController { + public: + MockContentControllerDelayed() + : mTime(SampleTime::FromTest(GetStartupTime())) {} + + const TimeStamp& Time() { return mTime.Time(); } + const SampleTime& GetSampleTime() { return mTime; } + + void AdvanceByMillis(int aMillis) { + AdvanceBy(TimeDuration::FromMilliseconds(aMillis)); + } + + void AdvanceBy(const TimeDuration& aIncrement) { + SampleTime target = mTime + aIncrement; + while (mTaskQueue.Length() > 0 && mTaskQueue[0].second <= target) { + RunNextDelayedTask(); + } + mTime = target; + } + + void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) { + RefPtr<Runnable> task = aTask; + SampleTime runAtTime = mTime + TimeDuration::FromMilliseconds(aDelayMs); + int insIndex = mTaskQueue.Length(); + while (insIndex > 0) { + if (mTaskQueue[insIndex - 1].second <= runAtTime) { + break; + } + insIndex--; + } + mTaskQueue.InsertElementAt(insIndex, std::make_pair(task, runAtTime)); + } + + // Run all the tasks in the queue, returning the number of tasks + // run. Note that if a task queues another task while running, that + // new task will not be run. Therefore, there may be still be tasks + // in the queue after this function is called. Only when the return + // value is 0 is the queue guaranteed to be empty. + int RunThroughDelayedTasks() { + nsTArray<std::pair<RefPtr<Runnable>, SampleTime>> runQueue = + std::move(mTaskQueue); + int numTasks = runQueue.Length(); + for (int i = 0; i < numTasks; i++) { + mTime = runQueue[i].second; + runQueue[i].first->Run(); + + // Deleting the task is important in order to release the reference to + // the callee object. + runQueue[i].first = nullptr; + } + return numTasks; + } + + private: + void RunNextDelayedTask() { + std::pair<RefPtr<Runnable>, SampleTime> next = mTaskQueue[0]; + mTaskQueue.RemoveElementAt(0); + mTime = next.second; + next.first->Run(); + // Deleting the task is important in order to release the reference to + // the callee object. + next.first = nullptr; + } + + // The following array is sorted by timestamp (tasks are inserted in order by + // timestamp). + nsTArray<std::pair<RefPtr<Runnable>, SampleTime>> mTaskQueue; + SampleTime mTime; +}; + +class TestAPZCTreeManager : public APZCTreeManager { + public: + explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc, + UniquePtr<IAPZHitTester> aHitTester = nullptr) + : APZCTreeManager(LayersId{0}, std::move(aHitTester)), mcc(aMcc) { + Init(); + } + + RefPtr<InputQueue> GetInputQueue() const { return mInputQueue; } + + void ClearContentController() { mcc = nullptr; } + + /** + * This function is not currently implemented. + * See bug 1468804 for more information. + **/ + void CancelAnimation() { EXPECT_TRUE(false); } + + bool AdvanceAnimations(const SampleTime& aSampleTime) { + MutexAutoLock lock(mMapLock); + return AdvanceAnimationsInternal(lock, aSampleTime); + } + + APZEventResult ReceiveInputEvent( + InputData& aEvent, + InputBlockCallback&& aCallback = InputBlockCallback()) override { + APZEventResult result = + APZCTreeManager::ReceiveInputEvent(aEvent, std::move(aCallback)); + if (aEvent.mInputType == PANGESTURE_INPUT && + // In the APZCTreeManager::ReceiveInputEvent some type of pan gesture + // events are marked as `mHandledByAPZ = false` (e.g. with Ctrl key + // modifier which causes reflow zoom), in such cases the events will + // never be processed by InputQueue so we shouldn't try to invoke + // AllowsSwipe() here. + aEvent.AsPanGestureInput().mHandledByAPZ && + aEvent.AsPanGestureInput().AllowsSwipe()) { + SetBrowserGestureResponse(result.mInputBlockId, + BrowserGestureResponse::NotConsumed); + } + return result; + } + + protected: + already_AddRefed<AsyncPanZoomController> NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController) override; + + SampleTime GetFrameTime() override { return mcc->GetSampleTime(); } + + private: + RefPtr<MockContentControllerDelayed> mcc; +}; + +class TestAsyncPanZoomController : public AsyncPanZoomController { + public: + TestAsyncPanZoomController(LayersId aLayersId, + MockContentControllerDelayed* aMcc, + TestAPZCTreeManager* aTreeManager, + GestureBehavior aBehavior = DEFAULT_GESTURES) + : AsyncPanZoomController(aLayersId, aTreeManager, + aTreeManager->GetInputQueue(), aMcc, aBehavior), + mWaitForMainThread(false), + mcc(aMcc) {} + + APZEventResult ReceiveInputEvent( + InputData& aEvent, + const Maybe<nsTArray<uint32_t>>& aTouchBehaviors = Nothing()) { + // This is a function whose signature matches exactly the ReceiveInputEvent + // on APZCTreeManager. This allows us to templates for functions like + // TouchDown, TouchUp, etc so that we can reuse the code for dispatching + // events into both APZC and APZCTM. + APZEventResult result = GetInputQueue()->ReceiveInputEvent( + this, TargetConfirmationFlags{!mWaitForMainThread}, aEvent, + aTouchBehaviors); + + if (aEvent.mInputType == PANGESTURE_INPUT && + aEvent.AsPanGestureInput().AllowsSwipe()) { + GetInputQueue()->SetBrowserGestureResponse( + result.mInputBlockId, BrowserGestureResponse::NotConsumed); + } + return result; + } + + void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault) { + GetInputQueue()->ContentReceivedInputBlock(aInputBlockId, aPreventDefault); + } + + void ConfirmTarget(uint64_t aInputBlockId) { + RefPtr<AsyncPanZoomController> target = this; + GetInputQueue()->SetConfirmedTargetApzc(aInputBlockId, target); + } + + void SetAllowedTouchBehavior(uint64_t aInputBlockId, + const nsTArray<TouchBehaviorFlags>& aBehaviors) { + GetInputQueue()->SetAllowedTouchBehavior(aInputBlockId, aBehaviors); + } + + void SetFrameMetrics(const FrameMetrics& metrics) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + Metrics() = metrics; + } + + void SetScrollMetadata(const ScrollMetadata& aMetadata) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mScrollMetadata = aMetadata; + } + + FrameMetrics& GetFrameMetrics() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics(); + } + + ScrollMetadata& GetScrollMetadata() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata; + } + + const FrameMetrics& GetFrameMetrics() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics(); + } + + using AsyncPanZoomController::GetOverscrollAmount; + using AsyncPanZoomController::GetVelocityVector; + + void AssertStateIsReset() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(NOTHING, mState); + } + + void AssertStateIsFling() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(FLING, mState); + } + + void AssertStateIsSmoothScroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(SMOOTH_SCROLL, mState); + } + + void AssertStateIsSmoothMsdScroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(SMOOTHMSD_SCROLL, mState); + } + + void AssertStateIsPanningLockedY() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING_LOCKED_Y, mState); + } + + void AssertStateIsPanningLockedX() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING_LOCKED_X, mState); + } + + void AssertStateIsPanning() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING, mState); + } + + void AssertStateIsPanMomentum() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PAN_MOMENTUM, mState); + } + + void AssertStateIsWheelScroll() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(WHEEL_SCROLL, mState); + } + + void SetAxisLocked(ScrollDirections aDirections, bool aLockValue) { + if (aDirections.contains(ScrollDirection::eVertical)) { + mY.SetAxisLocked(aLockValue); + } + if (aDirections.contains(ScrollDirection::eHorizontal)) { + mX.SetAxisLocked(aLockValue); + } + } + + void AssertNotAxisLocked() const { + EXPECT_FALSE(mY.IsAxisLocked()); + EXPECT_FALSE(mX.IsAxisLocked()); + } + + void AssertAxisLocked(ScrollDirection aDirection) const { + switch (aDirection) { + case ScrollDirection::eHorizontal: + EXPECT_TRUE(mY.IsAxisLocked()); + EXPECT_FALSE(mX.IsAxisLocked()); + break; + case ScrollDirection::eVertical: + EXPECT_TRUE(mX.IsAxisLocked()); + EXPECT_FALSE(mY.IsAxisLocked()); + break; + default: + FAIL() << "input direction must be either vertical or horizontal"; + } + } + + void AdvanceAnimationsUntilEnd( + const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(10)) { + while (AdvanceAnimations(mcc->GetSampleTime())) { + mcc->AdvanceBy(aIncrement); + } + } + + bool SampleContentTransformForFrame( + AsyncTransform* aOutTransform, ParentLayerPoint& aScrollOffset, + const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(0)) { + mcc->AdvanceBy(aIncrement); + bool ret = AdvanceAnimations(mcc->GetSampleTime()); + if (aOutTransform) { + *aOutTransform = + GetCurrentAsyncTransform(AsyncPanZoomController::eForEventHandling); + } + aScrollOffset = + GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForEventHandling); + return ret; + } + + CSSPoint GetCompositedScrollOffset() const { + return GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) / + GetFrameMetrics().GetZoom(); + } + + void SetWaitForMainThread() { mWaitForMainThread = true; } + + bool IsOverscrollAnimationRunning() const { + return mState == PanZoomState::OVERSCROLL_ANIMATION; + } + + bool IsWheelScrollAnimationRunning() const { + return mState == PanZoomState::WHEEL_SCROLL; + } + + private: + bool mWaitForMainThread; + MockContentControllerDelayed* mcc; +}; + +class APZCTesterBase : public ::testing::Test { + public: + APZCTesterBase() { mcc = new NiceMock<MockContentControllerDelayed>(); } + + void SetUp() override { + gfxPlatform::GetPlatform(); + // This pref is changed in Pan() without using SCOPED_GFX_PREF + // because the modified value needs to be in place until the touch + // events are processed, which may not happen until the input queue + // is flushed in TearDown(). So, we save and restore its value here. + mTouchStartTolerance = StaticPrefs::apz_touch_start_tolerance(); + } + + void TearDown() override { + Preferences::SetFloat("apz.touch_start_tolerance", mTouchStartTolerance); + } + + enum class PanOptions { + None = 0, + KeepFingerDown = 0x1, + /* + * Do not adjust the touch-start coordinates to overcome the touch-start + * tolerance threshold. If this option is passed, it's up to the caller + * to pass in coordinates that are sufficient to overcome the touch-start + * tolerance *and* cause the desired amount of scrolling. + */ + ExactCoordinates = 0x2, + NoFling = 0x4 + }; + + enum class PinchFlags { + None = 0, + LiftFinger1 = 0x1, + LiftFinger2 = 0x2, + /* + * The bitwise OR result of (LiftFinger1 | LiftFinger2). + * Defined explicitly here because it is used as the default + * argument for PinchWithTouchInput which is defined BEFORE the + * definition of operator| for this class. + */ + LiftBothFingers = 0x3 + }; + + template <class InputReceiver> + APZEventResult Tap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeDuration aTapLength, + nsEventStatus (*aOutEventStatuses)[2] = nullptr); + + template <class InputReceiver> + void TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeDuration aTapLength); + + template <class InputReceiver> + void Pan(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd, + PanOptions aOptions = PanOptions::None, + nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + /* + * A version of Pan() that only takes y coordinates rather than (x, y) points + * for the touch start and end points, and uses 10 for the x coordinates. + * This is for convenience, as most tests only need to pan in one direction. + */ + template <class InputReceiver> + void Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY, + int aTouchEndY, PanOptions aOptions = PanOptions::None, + nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + /* + * Dispatches mock touch events to the apzc and checks whether apzc properly + * consumed them and triggered scrolling behavior. + */ + template <class InputReceiver> + void PanAndCheckStatus(const RefPtr<InputReceiver>& aTarget, int aTouchStartY, + int aTouchEndY, bool aExpectConsumed, + nsTArray<uint32_t>* aAllowedTouchBehaviors, + uint64_t* aOutInputBlockId = nullptr); + + template <class InputReceiver> + void DoubleTap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t (*aOutInputBlockIds)[2] = nullptr); + + template <class InputReceiver> + void DoubleTapAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + uint64_t (*aOutInputBlockIds)[2] = nullptr); + + struct PinchOptions { + nsTArray<uint32_t>* mAllowedTouchBehaviors = nullptr; + nsEventStatus (*mOutEventStatuses)[4] = nullptr; + uint64_t* mOutInputBlockId = nullptr; + PinchFlags mFlags = PinchFlags::LiftBothFingers; + bool mVertical = false; + int* mInputId = nullptr; + Maybe<ScreenIntPoint> mSecondFocus; + TimeDuration mTimeBetweenTouchEvents = TimeDuration::FromMilliseconds(20); + + // Workaround for https://github.com/llvm/llvm-project/issues/36032 + PinchOptions() {} + + // Fluent interface + PinchOptions& AllowedTouchBehaviors( + nsTArray<uint32_t>* aAllowedTouchBehaviors) { + mAllowedTouchBehaviors = aAllowedTouchBehaviors; + return *this; + } + PinchOptions& OutEventStatuses(nsEventStatus (*aOutEventStatuses)[4]) { + mOutEventStatuses = aOutEventStatuses; + return *this; + } + PinchOptions& OutInputBlockId(uint64_t* aOutInputBlockId) { + mOutInputBlockId = aOutInputBlockId; + return *this; + } + PinchOptions& Flags(PinchFlags aFlags) { + mFlags = aFlags; + return *this; + } + PinchOptions& Vertical(bool aVertical) { + mVertical = aVertical; + return *this; + } + PinchOptions& InputId(int& aInputId) { + mInputId = &aInputId; + return *this; + } + PinchOptions& SecondFocus(const ScreenIntPoint& aSecondFocus) { + mSecondFocus = Some(aSecondFocus); + return *this; + } + PinchOptions& TimeBetweenTouchEvents(const TimeDuration& aDuration) { + mTimeBetweenTouchEvents = aDuration; + return *this; + } + }; + + // Pinch with one focus point. Zooms in place with no panning + template <class InputReceiver> + void PinchWithTouchInput(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, float aScale, + PinchOptions aOptions = PinchOptions()); + + template <class InputReceiver> + void PinchWithTouchInputAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, bool aShouldTriggerPinch, + nsTArray<uint32_t>* aAllowedTouchBehaviors); + + template <class InputReceiver> + void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, + nsEventStatus (*aOutEventStatuses)[3] = nullptr); + + template <class InputReceiver> + void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, + float aScale, + bool aShouldTriggerPinch); + + protected: + RefPtr<MockContentControllerDelayed> mcc; + + private: + float mTouchStartTolerance; +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PanOptions) +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PinchFlags) + +template <class InputReceiver> +APZEventResult APZCTesterBase::Tap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + TimeDuration aTapLength, + nsEventStatus (*aOutEventStatuses)[2]) { + APZEventResult touchDownResult = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = touchDownResult.GetStatus(); + } + mcc->AdvanceBy(aTapLength); + + // If touch-action is enabled then simulate the allowed touch behaviour + // notification that the main thread is supposed to deliver. + if (touchDownResult.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, touchDownResult.mInputBlockId); + } + + APZEventResult touchUpResult = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = touchUpResult.GetStatus(); + } + return touchDownResult; +} + +template <class InputReceiver> +void APZCTesterBase::TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + TimeDuration aTapLength) { + nsEventStatus statuses[2]; + Tap(aTarget, aPoint, aTapLength, &statuses); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]); +} + +template <class InputReceiver> +void APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aTouchStart, + const ScreenIntPoint& aTouchEnd, PanOptions aOptions, + nsTArray<uint32_t>* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t* aOutInputBlockId) { + // Reduce the move tolerance to a tiny value. + // We can't use a scoped pref because this value might be read at some later + // time when the events are actually processed, rather than when we deliver + // them. + const float touchStartTolerance = 0.1f; + const float panThreshold = touchStartTolerance * aTarget->GetDPI(); + Preferences::SetFloat("apz.touch_start_tolerance", touchStartTolerance); + Preferences::SetFloat("apz.touch_move_tolerance", 0.0f); + int overcomeTouchToleranceX = 0; + int overcomeTouchToleranceY = 0; + if (!(aOptions & PanOptions::ExactCoordinates)) { + // Have the direction of the adjustment to overcome the touch tolerance + // match the direction of the entire gesture, otherwise we run into + // trouble such as accidentally activating the axis lock. + if (aTouchStart.x != aTouchEnd.x && aTouchStart.y != aTouchEnd.y) { + // Tests that need to avoid rounding error here can arrange for + // panThreshold to be 10 (by setting the DPI to 100), which makes sure + // that these are the legs in a Pythagorean triple where panThreshold is + // the hypotenuse. Watch out for changes of APZCPinchTester::mDPI. + overcomeTouchToleranceX = panThreshold / 10 * 6; + overcomeTouchToleranceY = panThreshold / 10 * 8; + } else if (aTouchStart.x != aTouchEnd.x) { + overcomeTouchToleranceX = panThreshold; + } else if (aTouchStart.y != aTouchEnd.y) { + overcomeTouchToleranceY = panThreshold; + } + } + + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(20); + + // Even if the caller doesn't care about the block id, we need it to set the + // allowed touch behaviour below, so make sure aOutInputBlockId is non-null. + uint64_t blockId; + if (!aOutInputBlockId) { + aOutInputBlockId = &blockId; + } + + // Make sure the move is large enough to not be handled as a tap + APZEventResult result = + TouchDown(aTarget, + ScreenIntPoint(aTouchStart.x + overcomeTouchToleranceX, + aTouchStart.y + overcomeTouchToleranceY), + mcc->Time()); + if (aOutInputBlockId) { + *aOutInputBlockId = result.mInputBlockId; + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Allowed touch behaviours must be set after sending touch-start. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + if (aAllowedTouchBehaviors) { + EXPECT_EQ(1UL, aAllowedTouchBehaviors->Length()); + aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, + *aAllowedTouchBehaviors); + } else { + SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId); + } + } + + result = TouchMove(aTarget, aTouchStart, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + const int numSteps = 3; + auto stepVector = (aTouchEnd - aTouchStart) / numSteps; + for (int k = 1; k < numSteps; k++) { + auto stepPoint = aTouchStart + stepVector * k; + Unused << TouchMove(aTarget, stepPoint, mcc->Time()); + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + } + + result = TouchMove(aTarget, aTouchEnd, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + if (!(aOptions & PanOptions::KeepFingerDown)) { + result = TouchUp(aTarget, aTouchEnd, mcc->Time()); + } else { + result.SetStatusAsIgnore(); + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } + + if ((aOptions & PanOptions::NoFling)) { + aTarget->CancelAnimation(); + } + + // Don't increment the time here. Animations started on touch-up, such as + // flings, are affected by elapsed time, and we want to be able to sample + // them immediately after they start, without time having elapsed. +} + +template <class InputReceiver> +void APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY, + int aTouchEndY, PanOptions aOptions, + nsTArray<uint32_t>* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t* aOutInputBlockId) { + Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY), + aOptions, aAllowedTouchBehaviors, aOutEventStatuses, aOutInputBlockId); +} + +template <class InputReceiver> +void APZCTesterBase::PanAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, int aTouchStartY, int aTouchEndY, + bool aExpectConsumed, nsTArray<uint32_t>* aAllowedTouchBehaviors, + uint64_t* aOutInputBlockId) { + nsEventStatus statuses[4]; // down, move, move, up + Pan(aTarget, aTouchStartY, aTouchEndY, PanOptions::None, + aAllowedTouchBehaviors, &statuses, aOutInputBlockId); + + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + + nsEventStatus touchMoveStatus; + if (aExpectConsumed) { + touchMoveStatus = nsEventStatus_eConsumeDoDefault; + } else { + touchMoveStatus = nsEventStatus_eIgnore; + } + EXPECT_EQ(touchMoveStatus, statuses[1]); + EXPECT_EQ(touchMoveStatus, statuses[2]); +} + +template <class InputReceiver> +void APZCTesterBase::DoubleTap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t (*aOutInputBlockIds)[2]) { + APZEventResult result = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + if (aOutInputBlockIds) { + (*aOutInputBlockIds)[0] = result.mInputBlockId; + } + mcc->AdvanceByMillis(10); + + // If touch-action is enabled then simulate the allowed touch behaviour + // notification that the main thread is supposed to deliver. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId); + } + + result = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + mcc->AdvanceByMillis(10); + result = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + if (aOutInputBlockIds) { + (*aOutInputBlockIds)[1] = result.mInputBlockId; + } + mcc->AdvanceByMillis(10); + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId); + } + + result = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } +} + +template <class InputReceiver> +void APZCTesterBase::DoubleTapAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint, + uint64_t (*aOutInputBlockIds)[2]) { + nsEventStatus statuses[4]; + DoubleTap(aTarget, aPoint, &statuses, aOutInputBlockIds); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[2]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[3]); +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithTouchInput(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, + float aScale, PinchOptions aOptions) { + // Having pinch coordinates in float type may cause problems with + // high-precision scale values since SingleTouchData accepts integer value. + // But for trivial tests it should be ok. + const float pinchLength = 100.0; + const float pinchLengthScaled = pinchLength * aScale; + + const float pinchLengthX = aOptions.mVertical ? 0 : pinchLength; + const float pinchLengthScaledX = aOptions.mVertical ? 0 : pinchLengthScaled; + const float pinchLengthY = aOptions.mVertical ? pinchLength : 0; + const float pinchLengthScaledY = aOptions.mVertical ? pinchLengthScaled : 0; + + // Even if the caller doesn't care about the block id, we need it to set the + // allowed touch behaviour below, so make sure aOutInputBlockId is non-null. + uint64_t blockId; + if (!aOptions.mOutInputBlockId) { + aOptions.mOutInputBlockId = &blockId; + } + + int inputId = aOptions.mInputId ? *aOptions.mInputId : 0; + + // If a second focus point is not specified in the pinch options, use the + // same focus point throughout the gesture. + ScreenIntPoint secondFocus = + aOptions.mSecondFocus.isSome() ? *aOptions.mSecondFocus : aFocus; + + MultiTouchInput mtiStart = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus)); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus)); + APZEventResult result; + result = aTarget->ReceiveInputEvent(mtiStart); + if (aOptions.mOutInputBlockId) { + *aOptions.mOutInputBlockId = result.mInputBlockId; + } + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[0] = result.GetStatus(); + } + + if (aOptions.mAllowedTouchBehaviors) { + EXPECT_EQ(2UL, aOptions.mAllowedTouchBehaviors->Length()); + aTarget->SetAllowedTouchBehavior(*aOptions.mOutInputBlockId, + *aOptions.mAllowedTouchBehaviors); + } else { + SetDefaultAllowedTouchBehavior(aTarget, *aOptions.mOutInputBlockId, 2); + } + + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + + ScreenIntPoint pinchStartPoint1(aFocus.x - int32_t(pinchLengthX), + aFocus.y - int32_t(pinchLengthY)); + ScreenIntPoint pinchStartPoint2(aFocus.x + int32_t(pinchLengthX), + aFocus.y + int32_t(pinchLengthY)); + + MultiTouchInput mtiMove1 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove1.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchStartPoint1)); + mtiMove1.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchStartPoint2)); + result = aTarget->ReceiveInputEvent(mtiMove1); + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[1] = result.GetStatus(); + } + + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + + // Pinch instantly but move in steps. + const int numSteps = 3; + auto stepVector = (secondFocus - aFocus) / numSteps; + for (int k = 1; k < numSteps; k++) { + ScreenIntPoint stepFocus = aFocus + stepVector * k; + ScreenIntPoint stepPoint1(stepFocus.x - int32_t(pinchLengthScaledX), + stepFocus.y - int32_t(pinchLengthScaledY)); + ScreenIntPoint stepPoint2(stepFocus.x + int32_t(pinchLengthScaledX), + stepFocus.y + int32_t(pinchLengthScaledY)); + MultiTouchInput mtiMoveStep = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMoveStep.mTouches.AppendElement( + CreateSingleTouchData(inputId, stepPoint1)); + mtiMoveStep.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, stepPoint2)); + Unused << aTarget->ReceiveInputEvent(mtiMoveStep); + + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + } + + ScreenIntPoint pinchEndPoint1(secondFocus.x - int32_t(pinchLengthScaledX), + secondFocus.y - int32_t(pinchLengthScaledY)); + ScreenIntPoint pinchEndPoint2(secondFocus.x + int32_t(pinchLengthScaledX), + secondFocus.y + int32_t(pinchLengthScaledY)); + + MultiTouchInput mtiMove2 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove2.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchEndPoint1)); + mtiMove2.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchEndPoint2)); + result = aTarget->ReceiveInputEvent(mtiMove2); + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[2] = result.GetStatus(); + } + + if (aOptions.mFlags & (PinchFlags::LiftFinger1 | PinchFlags::LiftFinger2)) { + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + + MultiTouchInput mtiEnd = + MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + if (aOptions.mFlags & PinchFlags::LiftFinger1) { + mtiEnd.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchEndPoint1)); + } + if (aOptions.mFlags & PinchFlags::LiftFinger2) { + mtiEnd.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchEndPoint2)); + } + result = aTarget->ReceiveInputEvent(mtiEnd); + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[3] = result.GetStatus(); + } + } + + inputId += 2; + + if (aOptions.mInputId) { + *aOptions.mInputId = inputId; + } +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithTouchInputAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, bool aShouldTriggerPinch, + nsTArray<uint32_t>* aAllowedTouchBehaviors) { + nsEventStatus statuses[4]; // down, move, move, up + PinchWithTouchInput(aTarget, aFocus, aScale, + PinchOptions() + .AllowedTouchBehaviors(aAllowedTouchBehaviors) + .OutEventStatuses(&statuses) + .InputId(inputId)); + + nsEventStatus expectedMoveStatus = aShouldTriggerPinch + ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(expectedMoveStatus, statuses[1]); + EXPECT_EQ(expectedMoveStatus, statuses[2]); +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithPinchInput( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, + nsEventStatus (*aOutEventStatuses)[3]) { + const TimeDuration TIME_BETWEEN_PINCH_INPUT = + TimeDuration::FromMilliseconds(50); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + aFocus, 10.0, 10.0, mcc->Time()); + APZEventResult actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = actual.GetStatus(); + } + mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + aSecondFocus, 10.0 * aScale, 10.0, mcc->Time()); + actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = actual.GetStatus(); + } + mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_END, aSecondFocus, + 10.0 * aScale, 10.0 * aScale, mcc->Time()); + actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = actual.GetStatus(); + } +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithPinchInputAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + float aScale, bool aShouldTriggerPinch) { + nsEventStatus statuses[3]; // scalebegin, scale, scaleend + PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses); + + nsEventStatus expectedStatus = aShouldTriggerPinch + ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + EXPECT_EQ(expectedStatus, statuses[0]); + EXPECT_EQ(expectedStatus, statuses[1]); +} + +inline FrameMetrics TestFrameMetrics() { + FrameMetrics fm; + + fm.SetDisplayPort(CSSRect(0, 0, 10, 10)); + fm.SetCompositionBounds(ParentLayerRect(0, 0, 10, 10)); + fm.SetScrollableRect(CSSRect(0, 0, 100, 100)); + + return fm; +} + +#endif // mozilla_layers_APZTestCommon_h |