/* -*- Mode: C++; tab-width: 8; 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 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/AsyncCompositionManager.h" // for ViewTransform #include "mozilla/layers/GeckoContentController.h" #include "mozilla/layers/CompositorBridgeParent.h" #include "mozilla/layers/APZCTreeManager.h" #include "mozilla/layers/LayerMetricsWrapper.h" #include "mozilla/layers/APZThreadUtils.h" #include "mozilla/UniquePtr.h" #include "apz/src/AsyncPanZoomController.h" #include "apz/src/HitTestingTreeNode.h" #include "base/task.h" #include "Layers.h" #include "TestLayers.h" #include "UnitTransforms.h" #include "gfxPrefs.h" using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::layers; using ::testing::_; using ::testing::NiceMock; using ::testing::AtLeast; using ::testing::AtMost; using ::testing::MockFunction; using ::testing::InSequence; typedef mozilla::layers::GeckoContentController::TapType TapType; template class ScopedGfxPref { public: ScopedGfxPref(T (*aGetPrefFunc)(void), void (*aSetPrefFunc)(T), T aVal) : mSetPrefFunc(aSetPrefFunc) { mOldVal = aGetPrefFunc(); aSetPrefFunc(aVal); } ~ScopedGfxPref() { mSetPrefFunc(mOldVal); } private: void (*mSetPrefFunc)(T); T mOldVal; }; #define SCOPED_GFX_PREF(prefBase, prefType, prefValue) \ ScopedGfxPref pref_##prefBase( \ &(gfxPrefs::prefBase), \ &(gfxPrefs::Set##prefBase), \ prefValue) static TimeStamp GetStartupTime() { static TimeStamp sStartupTime = TimeStamp::Now(); return sStartupTime; } class MockContentController : public GeckoContentController { public: MOCK_METHOD1(RequestContentRepaint, void(const FrameMetrics&)); MOCK_METHOD2(RequestFlingSnap, void(const FrameMetrics::ViewID& aScrollId, const mozilla::CSSPoint& aDestination)); MOCK_METHOD2(AcknowledgeScrollUpdate, void(const FrameMetrics::ViewID&, const uint32_t& aScrollGeneration)); MOCK_METHOD5(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers, const ScrollableLayerGuid&, uint64_t)); MOCK_METHOD4(NotifyPinchGesture, void(PinchGestureInput::PinchGestureType, const ScrollableLayerGuid&, 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(Move(aTask)); } MOCK_METHOD3(NotifyAPZStateChange, void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg)); MOCK_METHOD0(NotifyFlushComplete, void()); }; class MockContentControllerDelayed : public MockContentController { public: MockContentControllerDelayed() : mTime(GetStartupTime()) { } const TimeStamp& Time() { return mTime; } void AdvanceByMillis(int aMillis) { AdvanceBy(TimeDuration::FromMilliseconds(aMillis)); } void AdvanceBy(const TimeDuration& aIncrement) { TimeStamp target = mTime + aIncrement; while (mTaskQueue.Length() > 0 && mTaskQueue[0].second <= target) { RunNextDelayedTask(); } mTime = target; } void PostDelayedTask(already_AddRefed aTask, int aDelayMs) { RefPtr task = aTask; TimeStamp 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, TimeStamp>> runQueue; runQueue.SwapElements(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, TimeStamp> 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, TimeStamp>> mTaskQueue; TimeStamp mTime; }; class TestAPZCTreeManager : public APZCTreeManager { public: explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc) : mcc(aMcc) {} RefPtr GetInputQueue() const { return mInputQueue; } void ClearContentController() { mcc = nullptr; } protected: AsyncPanZoomController* NewAPZCInstance(uint64_t aLayersId, GeckoContentController* aController) override; TimeStamp GetFrameTime() override { return mcc->Time(); } private: RefPtr mcc; }; class TestAsyncPanZoomController : public AsyncPanZoomController { public: TestAsyncPanZoomController(uint64_t aLayersId, MockContentControllerDelayed* aMcc, TestAPZCTreeManager* aTreeManager, GestureBehavior aBehavior = DEFAULT_GESTURES) : AsyncPanZoomController(aLayersId, aTreeManager, aTreeManager->GetInputQueue(), aMcc, aBehavior) , mWaitForMainThread(false) , mcc(aMcc) {} nsEventStatus ReceiveInputEvent(const InputData& aEvent, ScrollableLayerGuid* aDummy, uint64_t* aOutInputBlockId) { // 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. return ReceiveInputEvent(aEvent, aOutInputBlockId); } nsEventStatus ReceiveInputEvent(const InputData& aEvent, uint64_t* aOutInputBlockId) { return GetInputQueue()->ReceiveInputEvent(this, !mWaitForMainThread, aEvent, aOutInputBlockId); } 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) { ReentrantMonitorAutoEnter lock(mMonitor); mFrameMetrics = metrics; } FrameMetrics& GetFrameMetrics() { ReentrantMonitorAutoEnter lock(mMonitor); return mFrameMetrics; } ScrollMetadata& GetScrollMetadata() { ReentrantMonitorAutoEnter lock(mMonitor); return mScrollMetadata; } const FrameMetrics& GetFrameMetrics() const { ReentrantMonitorAutoEnter lock(mMonitor); return mFrameMetrics; } using AsyncPanZoomController::GetVelocityVector; void AssertStateIsReset() const { ReentrantMonitorAutoEnter lock(mMonitor); EXPECT_EQ(NOTHING, mState); } void AssertStateIsFling() const { ReentrantMonitorAutoEnter lock(mMonitor); EXPECT_EQ(FLING, mState); } void AdvanceAnimationsUntilEnd(const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(10)) { while (AdvanceAnimations(mcc->Time())) { mcc->AdvanceBy(aIncrement); } } bool SampleContentTransformForFrame(AsyncTransform* aOutTransform, ParentLayerPoint& aScrollOffset, const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(0)) { mcc->AdvanceBy(aIncrement); bool ret = AdvanceAnimations(mcc->Time()); if (aOutTransform) { *aOutTransform = GetCurrentAsyncTransform(AsyncPanZoomController::NORMAL); } aScrollOffset = GetCurrentAsyncScrollOffset(AsyncPanZoomController::NORMAL); return ret; } void SetWaitForMainThread() { mWaitForMainThread = true; } private: bool mWaitForMainThread; MockContentControllerDelayed* mcc; }; class APZCTesterBase : public ::testing::Test { public: APZCTesterBase() { mcc = new NiceMock(); } template void Tap(const RefPtr& aTarget, const ScreenIntPoint& aPoint, TimeDuration aTapLength, nsEventStatus (*aOutEventStatuses)[2] = nullptr, uint64_t* aOutInputBlockId = nullptr); template void TapAndCheckStatus(const RefPtr& aTarget, const ScreenIntPoint& aPoint, TimeDuration aTapLength); template void Pan(const RefPtr& aTarget, const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd, bool aKeepFingerDown = false, 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, bool aKeepFingerDown = false, 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); void ApzcPanNoFling(const RefPtr& aApzc, int aTouchStartY, int aTouchEndY, 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); protected: RefPtr mcc; }; template void APZCTesterBase::Tap(const RefPtr& aTarget, const ScreenIntPoint& aPoint, TimeDuration aTapLength, nsEventStatus (*aOutEventStatuses)[2], uint64_t* aOutInputBlockId) { // 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; } nsEventStatus status = TouchDown(aTarget, aPoint, mcc->Time(), aOutInputBlockId); if (aOutEventStatuses) { (*aOutEventStatuses)[0] = status; } mcc->AdvanceBy(aTapLength); // If touch-action is enabled then simulate the allowed touch behaviour // notification that the main thread is supposed to deliver. if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) { SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId); } status = TouchUp(aTarget, aPoint, mcc->Time()); if (aOutEventStatuses) { (*aOutEventStatuses)[1] = status; } } 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, bool aKeepFingerDown, nsTArray* aAllowedTouchBehaviors, nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId) { // Reduce the touch start and 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. gfxPrefs::SetAPZTouchStartTolerance(1.0f / 1000.0f); gfxPrefs::SetAPZTouchMoveTolerance(0.0f); const int OVERCOME_TOUCH_TOLERANCE = 1; const TimeDuration TIME_BETWEEN_TOUCH_EVENT = TimeDuration::FromMilliseconds(50); // 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 nsEventStatus status = TouchDown(aTarget, ScreenIntPoint(aTouchStart.x, aTouchStart.y + OVERCOME_TOUCH_TOLERANCE), mcc->Time(), aOutInputBlockId); if (aOutEventStatuses) { (*aOutEventStatuses)[0] = status; } mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); // Allowed touch behaviours must be set after sending touch-start. if (status != nsEventStatus_eConsumeNoDefault) { if (aAllowedTouchBehaviors) { EXPECT_EQ(1UL, aAllowedTouchBehaviors->Length()); aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, *aAllowedTouchBehaviors); } else if (gfxPrefs::TouchActionEnabled()) { SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId); } } status = TouchMove(aTarget, aTouchStart, mcc->Time()); if (aOutEventStatuses) { (*aOutEventStatuses)[1] = status; } mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); status = TouchMove(aTarget, aTouchEnd, mcc->Time()); if (aOutEventStatuses) { (*aOutEventStatuses)[2] = status; } mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); if (!aKeepFingerDown) { status = TouchUp(aTarget, aTouchEnd, mcc->Time()); } else { status = nsEventStatus_eIgnore; } if (aOutEventStatuses) { (*aOutEventStatuses)[3] = status; } // 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, bool aKeepFingerDown, nsTArray* aAllowedTouchBehaviors, nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId) { Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY), aKeepFingerDown, 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, false, 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]); } void APZCTesterBase::ApzcPanNoFling(const RefPtr& aApzc, int aTouchStartY, int aTouchEndY, uint64_t* aOutInputBlockId) { Pan(aApzc, aTouchStartY, aTouchEndY, false, nullptr, nullptr, aOutInputBlockId); aApzc->CancelAnimation(); } template void APZCTesterBase::DoubleTap(const RefPtr& aTarget, const ScreenIntPoint& aPoint, nsEventStatus (*aOutEventStatuses)[4], uint64_t (*aOutInputBlockIds)[2]) { uint64_t blockId; nsEventStatus status = TouchDown(aTarget, aPoint, mcc->Time(), &blockId); if (aOutEventStatuses) { (*aOutEventStatuses)[0] = status; } if (aOutInputBlockIds) { (*aOutInputBlockIds)[0] = blockId; } mcc->AdvanceByMillis(10); // If touch-action is enabled then simulate the allowed touch behaviour // notification that the main thread is supposed to deliver. if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) { SetDefaultAllowedTouchBehavior(aTarget, blockId); } status = TouchUp(aTarget, aPoint, mcc->Time()); if (aOutEventStatuses) { (*aOutEventStatuses)[1] = status; } mcc->AdvanceByMillis(10); status = TouchDown(aTarget, aPoint, mcc->Time(), &blockId); if (aOutEventStatuses) { (*aOutEventStatuses)[2] = status; } if (aOutInputBlockIds) { (*aOutInputBlockIds)[1] = blockId; } mcc->AdvanceByMillis(10); if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) { SetDefaultAllowedTouchBehavior(aTarget, blockId); } status = TouchUp(aTarget, aPoint, mcc->Time()); if (aOutEventStatuses) { (*aOutEventStatuses)[3] = status; } } 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]); } AsyncPanZoomController* TestAPZCTreeManager::NewAPZCInstance(uint64_t aLayersId, GeckoContentController* aController) { MockContentControllerDelayed* mcc = static_cast(aController); return new TestAsyncPanZoomController(aLayersId, mcc, this, AsyncPanZoomController::USE_GESTURE_DETECTOR); } FrameMetrics TestFrameMetrics() { FrameMetrics fm; fm.SetDisplayPort(CSSRect(0, 0, 10, 10)); fm.SetCompositionBounds(ParentLayerRect(0, 0, 10, 10)); fm.SetCriticalDisplayPort(CSSRect(0, 0, 10, 10)); fm.SetScrollableRect(CSSRect(0, 0, 100, 100)); return fm; } uint32_t MillisecondsSinceStartup(TimeStamp aTime) { return (aTime - GetStartupTime()).ToMilliseconds(); } #endif // mozilla_layers_APZTestCommon_h