/* -*- 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 ScopedGfxSetting { public: ScopedGfxSetting(const std::function& aGetPrefFunc, const std::function& aSetPrefFunc, SetArg aVal) : mSetPrefFunc(aSetPrefFunc) { mOldVal = aGetPrefFunc(); aSetPrefFunc(aVal); } ~ScopedGfxSetting() { mSetPrefFunc(mOldVal); } private: std::function 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 FRESH_PREF_VAR( \ [=]() { return Preferences::GetBool(prefName); }, \ [=](bool aPrefValue) { Preferences::SetBool(prefName, aPrefValue); }, \ prefValue) #define SCOPED_GFX_PREF_INT(prefName, prefValue) \ ScopedGfxSetting FRESH_PREF_VAR( \ [=]() { return Preferences::GetInt(prefName); }, \ [=](int32_t aPrefValue) { Preferences::SetInt(prefName, aPrefValue); }, \ prefValue) #define SCOPED_GFX_PREF_FLOAT(prefName, prefValue) \ ScopedGfxSetting FRESH_PREF_VAR( \ [=]() { return Preferences::GetFloat(prefName); }, \ [=](float aPrefValue) { Preferences::SetFloat(prefName, aPrefValue); }, \ prefValue) class MockContentController : public GeckoContentController { public: MOCK_METHOD1(NotifyLayerTransforms, void(nsTArray&&)); MOCK_METHOD1(RequestContentRepaint, void(const RepaintRequest&)); MOCK_METHOD6(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers, const ScrollableLayerGuid&, uint64_t, const Maybe&)); MOCK_METHOD5(NotifyPinchGesture, void(PinchGestureInput::PinchGestureType, const ScrollableLayerGuid&, const LayoutDevicePoint&, LayoutDeviceCoord, Modifiers)); // Can't use the macros with already_AddRefed :( void PostDelayedTask(already_AddRefed aTask, int aDelayMs) { RefPtr task = aTask; } bool IsRepaintThread() { return NS_IsMainThread(); } void DispatchToRepaintThread(already_AddRefed aTask) { NS_DispatchToMainThread(std::move(aTask)); } MOCK_METHOD4(NotifyAPZStateChange, void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg, Maybe 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 aTask, int aDelayMs) { RefPtr 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, 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, 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, SampleTime>> mTaskQueue; SampleTime mTime; }; class TestAPZCTreeManager : public APZCTreeManager { public: explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc, UniquePtr aHitTester = nullptr) : APZCTreeManager(LayersId{0}, std::move(aHitTester)), mcc(aMcc) { Init(); } RefPtr 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 NewAPZCInstance( LayersId aLayersId, GeckoContentController* aController) override; SampleTime GetFrameTime() override { return mcc->GetSampleTime(); } private: RefPtr 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>& 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 target = this; GetInputQueue()->SetConfirmedTargetApzc(aInputBlockId, target); } void SetAllowedTouchBehavior(uint64_t aInputBlockId, const nsTArray& 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(); } 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 APZEventResult Tap(const RefPtr& aTarget, const ScreenIntPoint& aPoint, TimeDuration aTapLength, nsEventStatus (*aOutEventStatuses)[2] = nullptr); template void TapAndCheckStatus(const RefPtr& aTarget, const ScreenIntPoint& aPoint, TimeDuration aTapLength); template void Pan(const RefPtr& aTarget, const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd, PanOptions aOptions = PanOptions::None, nsTArray* 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 void Pan(const RefPtr& aTarget, int aTouchStartY, int aTouchEndY, PanOptions aOptions = PanOptions::None, nsTArray* 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 void PanAndCheckStatus(const RefPtr& aTarget, int aTouchStartY, int aTouchEndY, bool aExpectConsumed, nsTArray* aAllowedTouchBehaviors, uint64_t* aOutInputBlockId = nullptr); template void DoubleTap(const RefPtr& aTarget, const ScreenIntPoint& aPoint, nsEventStatus (*aOutEventStatuses)[4] = nullptr, uint64_t (*aOutInputBlockIds)[2] = nullptr); template void DoubleTapAndCheckStatus(const RefPtr& aTarget, const ScreenIntPoint& aPoint, uint64_t (*aOutInputBlockIds)[2] = nullptr); struct PinchOptions { nsTArray* mAllowedTouchBehaviors = nullptr; nsEventStatus (*mOutEventStatuses)[4] = nullptr; uint64_t* mOutInputBlockId = nullptr; PinchFlags mFlags = PinchFlags::LiftBothFingers; bool mVertical = false; int* mInputId = nullptr; Maybe mSecondFocus; TimeDuration mTimeBetweenTouchEvents = TimeDuration::FromMilliseconds(20); // Workaround for https://github.com/llvm/llvm-project/issues/36032 PinchOptions() {} // Fluent interface PinchOptions& AllowedTouchBehaviors( nsTArray* 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 void PinchWithTouchInput(const RefPtr& aTarget, const ScreenIntPoint& aFocus, float aScale, PinchOptions aOptions = PinchOptions()); template void PinchWithTouchInputAndCheckStatus( const RefPtr& aTarget, const ScreenIntPoint& aFocus, float aScale, int& inputId, bool aShouldTriggerPinch, nsTArray* aAllowedTouchBehaviors); template void PinchWithPinchInput(const RefPtr& aTarget, const ScreenIntPoint& aFocus, const ScreenIntPoint& aSecondFocus, float aScale, nsEventStatus (*aOutEventStatuses)[3] = nullptr); template void PinchWithPinchInputAndCheckStatus(const RefPtr& aTarget, const ScreenIntPoint& aFocus, float aScale, bool aShouldTriggerPinch); protected: RefPtr mcc; private: float mTouchStartTolerance; }; MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PanOptions) MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PinchFlags) template APZEventResult APZCTesterBase::Tap(const RefPtr& 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 void APZCTesterBase::TapAndCheckStatus(const RefPtr& 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 void APZCTesterBase::Pan(const RefPtr& aTarget, const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd, PanOptions aOptions, nsTArray* 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 void APZCTesterBase::Pan(const RefPtr& aTarget, int aTouchStartY, int aTouchEndY, PanOptions aOptions, nsTArray* aAllowedTouchBehaviors, nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId) { Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY), aOptions, aAllowedTouchBehaviors, aOutEventStatuses, aOutInputBlockId); } template void APZCTesterBase::PanAndCheckStatus( const RefPtr& aTarget, int aTouchStartY, int aTouchEndY, bool aExpectConsumed, nsTArray* 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 void APZCTesterBase::DoubleTap(const RefPtr& 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 void APZCTesterBase::DoubleTapAndCheckStatus( const RefPtr& 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 void APZCTesterBase::PinchWithTouchInput(const RefPtr& 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 void APZCTesterBase::PinchWithTouchInputAndCheckStatus( const RefPtr& aTarget, const ScreenIntPoint& aFocus, float aScale, int& inputId, bool aShouldTriggerPinch, nsTArray* 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 void APZCTesterBase::PinchWithPinchInput( const RefPtr& 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 void APZCTesterBase::PinchWithPinchInputAndCheckStatus( const RefPtr& 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