summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/test/gtest
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers/apz/test/gtest')
-rw-r--r--gfx/layers/apz/test/gtest/APZCBasicTester.h112
-rw-r--r--gfx/layers/apz/test/gtest/APZCTreeManagerTester.h222
-rw-r--r--gfx/layers/apz/test/gtest/APZTestAccess.cpp27
-rw-r--r--gfx/layers/apz/test/gtest/APZTestAccess.h36
-rw-r--r--gfx/layers/apz/test/gtest/APZTestCommon.cpp16
-rw-r--r--gfx/layers/apz/test/gtest/APZTestCommon.h1094
-rw-r--r--gfx/layers/apz/test/gtest/InputUtils.h151
-rw-r--r--gfx/layers/apz/test/gtest/MockHitTester.cpp76
-rw-r--r--gfx/layers/apz/test/gtest/MockHitTester.h43
-rw-r--r--gfx/layers/apz/test/gtest/TestAxisLock.cpp645
-rw-r--r--gfx/layers/apz/test/gtest/TestBasic.cpp791
-rw-r--r--gfx/layers/apz/test/gtest/TestEventRegions.cpp201
-rw-r--r--gfx/layers/apz/test/gtest/TestEventResult.cpp476
-rw-r--r--gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp252
-rw-r--r--gfx/layers/apz/test/gtest/TestGestureDetector.cpp845
-rw-r--r--gfx/layers/apz/test/gtest/TestHitTesting.cpp352
-rw-r--r--gfx/layers/apz/test/gtest/TestInputQueue.cpp48
-rw-r--r--gfx/layers/apz/test/gtest/TestOverscroll.cpp2029
-rw-r--r--gfx/layers/apz/test/gtest/TestPanning.cpp251
-rw-r--r--gfx/layers/apz/test/gtest/TestPinching.cpp664
-rw-r--r--gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp500
-rw-r--r--gfx/layers/apz/test/gtest/TestScrollHandoff.cpp809
-rw-r--r--gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp102
-rw-r--r--gfx/layers/apz/test/gtest/TestSnapping.cpp302
-rw-r--r--gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp102
-rw-r--r--gfx/layers/apz/test/gtest/TestTransformNotifications.cpp569
-rw-r--r--gfx/layers/apz/test/gtest/TestTreeManager.cpp411
-rw-r--r--gfx/layers/apz/test/gtest/TestWRScrollData.cpp273
-rw-r--r--gfx/layers/apz/test/gtest/TestWRScrollData.h63
-rw-r--r--gfx/layers/apz/test/gtest/moz.build40
-rw-r--r--gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp221
-rw-r--r--gfx/layers/apz/test/gtest/mvm/moz.build13
32 files changed, 11736 insertions, 0 deletions
diff --git a/gfx/layers/apz/test/gtest/APZCBasicTester.h b/gfx/layers/apz/test/gtest/APZCBasicTester.h
new file mode 100644
index 0000000000..13065f71f5
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZCBasicTester.h
@@ -0,0 +1,112 @@
+/* -*- 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_APZCBasicTester_h
+#define mozilla_layers_APZCBasicTester_h
+
+/**
+ * Defines a test fixture used for testing a single APZC.
+ */
+
+#include "APZTestCommon.h"
+
+#include "mozilla/layers/APZSampler.h"
+#include "mozilla/layers/APZUpdater.h"
+
+class APZCBasicTester : public APZCTesterBase {
+ public:
+ explicit APZCBasicTester(
+ AsyncPanZoomController::GestureBehavior aGestureBehavior =
+ AsyncPanZoomController::DEFAULT_GESTURES)
+ : mGestureBehavior(aGestureBehavior) {}
+
+ protected:
+ virtual void SetUp() {
+ APZCTesterBase::SetUp();
+ APZThreadUtils::SetThreadAssertionsEnabled(false);
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+
+ tm = new TestAPZCTreeManager(mcc);
+ updater = new APZUpdater(tm, false);
+ sampler = new APZSampler(tm, false);
+ apzc =
+ new TestAsyncPanZoomController(LayersId{0}, mcc, tm, mGestureBehavior);
+ apzc->SetFrameMetrics(TestFrameMetrics());
+ apzc->GetScrollMetadata().SetIsLayersIdRoot(true);
+ // Since we're working with just one APZC, make it the root-content one.
+ // Tests that want to test the behaviour of a non-root-content APZC
+ // generally want to do so in a context where it has a root-content
+ // ancestor, and so would use APZCTreeManagerTester.
+ // Note that some tests overwrite the initial FrameMetrics; such tests
+ // still need to take care that the root-content flag is set on the new
+ // FrameMetrics they set (if they care about root-content behaviours like
+ // zooming).
+ apzc->GetFrameMetrics().SetIsRootContent(true);
+ }
+
+ /**
+ * Get the APZC's scroll range in CSS pixels.
+ */
+ CSSRect GetScrollRange() const {
+ const FrameMetrics& metrics = apzc->GetFrameMetrics();
+ return CSSRect(metrics.GetScrollableRect().TopLeft(),
+ metrics.GetScrollableRect().Size() -
+ metrics.CalculateCompositedSizeInCssPixels());
+ }
+
+ virtual void TearDown() {
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ apzc->Destroy();
+ tm->ClearTree();
+ tm->ClearContentController();
+
+ APZCTesterBase::TearDown();
+ }
+
+ void MakeApzcWaitForMainThread() { apzc->SetWaitForMainThread(); }
+
+ void MakeApzcZoomable() {
+ MOZ_ASSERT(apzc->GetFrameMetrics().IsRootContent());
+ apzc->UpdateZoomConstraints(ZoomConstraints(
+ true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+ }
+
+ void MakeApzcUnzoomable() {
+ apzc->UpdateZoomConstraints(ZoomConstraints(false, false,
+ CSSToParentLayerScale(1.0f),
+ CSSToParentLayerScale(1.0f)));
+ }
+
+ /**
+ * Sample animations once, 1 ms later than the last sample.
+ */
+ void SampleAnimationOnce() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+ /**
+ * Sample animations one frame, 17 ms later than the last sample.
+ */
+ void SampleAnimationOneFrame() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(17);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+
+ AsyncPanZoomController::GestureBehavior mGestureBehavior;
+ RefPtr<TestAPZCTreeManager> tm;
+ RefPtr<APZSampler> sampler;
+ RefPtr<APZUpdater> updater;
+ RefPtr<TestAsyncPanZoomController> apzc;
+};
+
+#endif // mozilla_layers_APZCBasicTester_h
diff --git a/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h
new file mode 100644
index 0000000000..1f402bedc7
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h
@@ -0,0 +1,222 @@
+/* -*- 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_APZCTreeManagerTester_h
+#define mozilla_layers_APZCTreeManagerTester_h
+
+/**
+ * Defines a test fixture used for testing multiple APZCs interacting in
+ * an APZCTreeManager.
+ */
+
+#include "APZTestAccess.h"
+#include "APZTestCommon.h"
+#include "gfxPlatform.h"
+#include "MockHitTester.h"
+#include "apz/src/WRHitTester.h"
+
+#include "mozilla/layers/APZSampler.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+
+class APZCTreeManagerTester : public APZCTesterBase {
+ protected:
+ APZCTreeManagerTester() : mHitTester(MakeUnique<WRHitTester>()) {}
+
+ virtual void SetUp() {
+ APZCTesterBase::SetUp();
+
+ APZThreadUtils::SetThreadAssertionsEnabled(false);
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+
+ manager = new TestAPZCTreeManager(mcc, std::move(mHitTester));
+ updater = new APZUpdater(manager, false);
+ sampler = new APZSampler(manager, false);
+ }
+
+ virtual void TearDown() {
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ manager->ClearTree();
+ manager->ClearContentController();
+
+ APZCTesterBase::TearDown();
+ }
+
+ /**
+ * Sample animations once for all APZCs, 1 ms later than the last sample and
+ * return whether there is still any active animations or not.
+ */
+ bool SampleAnimationsOnce() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+
+ bool activeAnimations = false;
+
+ for (size_t i = 0; i < layers.GetLayerCount(); ++i) {
+ if (TestAsyncPanZoomController* apzc = ApzcOf(layers[i])) {
+ activeAnimations |=
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+ }
+
+ return activeAnimations;
+ }
+
+ // A convenience function for letting a test modify the frame metrics
+ // stored on a particular layer.
+ template <typename Callback>
+ void ModifyFrameMetrics(WebRenderLayerScrollData* aLayer,
+ Callback aCallback) {
+ MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 1);
+ ScrollMetadata& metadataRef =
+ APZTestAccess::GetScrollMetadataMut(*aLayer, layers, 0);
+ aCallback(metadataRef, metadataRef.GetMetrics());
+ }
+
+ // A convenience wrapper for manager->UpdateHitTestingTree().
+ void UpdateHitTestingTree(uint32_t aPaintSequenceNumber = 0) {
+ manager->UpdateHitTestingTree(WebRenderScrollDataWrapper{*updater, &layers},
+ /* is first paint = */ false, LayersId{0},
+ aPaintSequenceNumber);
+ }
+
+ void CreateScrollData(const char* aTreeShape,
+ const LayerIntRect* aVisibleRects = nullptr,
+ const gfx::Matrix4x4* aTransforms = nullptr) {
+ layers = TestWRScrollData::Create(aTreeShape, *updater, aVisibleRects,
+ aTransforms);
+ root = layers[0];
+ }
+
+ void CreateMockHitTester() {
+ mHitTester = MakeUnique<MockHitTester>();
+ // Save a pointer in a separate variable, because SetUp() will
+ // move the value out of mHitTester.
+ mMockHitTester = static_cast<MockHitTester*>(mHitTester.get());
+ }
+ void QueueMockHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ gfx::CompositorHitTestInfo aHitInfo =
+ gfx::CompositorHitTestFlags::eVisibleToHitTest) {
+ MOZ_ASSERT(mMockHitTester);
+ mMockHitTester->QueueHitResult(aScrollId, aHitInfo);
+ }
+
+ RefPtr<TestAPZCTreeManager> manager;
+ RefPtr<APZSampler> sampler;
+ RefPtr<APZUpdater> updater;
+ TestWRScrollData layers;
+ WebRenderLayerScrollData* root = nullptr;
+
+ UniquePtr<IAPZHitTester> mHitTester;
+ MockHitTester* mMockHitTester = nullptr;
+
+ protected:
+ static ScrollMetadata BuildScrollMetadata(
+ ScrollableLayerGuid::ViewID aScrollId, const CSSRect& aScrollableRect,
+ const ParentLayerRect& aCompositionBounds) {
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollId(aScrollId);
+ // By convention in this test file, START_SCROLL_ID is the root, so mark it
+ // as such.
+ if (aScrollId == ScrollableLayerGuid::START_SCROLL_ID) {
+ metadata.SetIsLayersIdRoot(true);
+ }
+ metrics.SetCompositionBounds(aCompositionBounds);
+ metrics.SetScrollableRect(aScrollableRect);
+ metrics.SetLayoutScrollOffset(CSSPoint(0, 0));
+ metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100));
+ metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10));
+ return metadata;
+ }
+
+ void SetScrollMetadata(WebRenderLayerScrollData* aLayer,
+ const ScrollMetadata& aMetadata) {
+ MOZ_ASSERT(aLayer->GetScrollMetadataCount() <= 1,
+ "This function does not support multiple ScrollMetadata on a "
+ "single layer");
+ if (aLayer->GetScrollMetadataCount() == 0) {
+ // Add new metrics
+ aLayer->AppendScrollMetadata(layers, aMetadata);
+ } else {
+ // Overwrite existing metrics
+ ModifyFrameMetrics(
+ aLayer, [&](ScrollMetadata& aSm, FrameMetrics&) { aSm = aMetadata; });
+ }
+ }
+
+ void SetScrollMetadata(WebRenderLayerScrollData* aLayer,
+ const nsTArray<ScrollMetadata>& aMetadata) {
+ // The reason for this restriction is that WebRenderLayerScrollData does not
+ // have an API to *remove* previous metadata.
+ MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 0,
+ "This function can only be used on layers which do not yet have "
+ "scroll metadata");
+ for (const ScrollMetadata& metadata : aMetadata) {
+ aLayer->AppendScrollMetadata(layers, metadata);
+ }
+ }
+
+ void SetScrollableFrameMetrics(WebRenderLayerScrollData* aLayer,
+ ScrollableLayerGuid::ViewID aScrollId,
+ CSSRect aScrollableRect = CSSRect(-1, -1, -1,
+ -1)) {
+ auto localTransform = aLayer->GetTransformTyped() * AsyncTransformMatrix();
+ ParentLayerIntRect compositionBounds = RoundedToInt(
+ localTransform.TransformBounds(LayerRect(aLayer->GetVisibleRect())));
+ ScrollMetadata metadata = BuildScrollMetadata(
+ aScrollId, aScrollableRect, ParentLayerRect(compositionBounds));
+ SetScrollMetadata(aLayer, metadata);
+ }
+
+ bool HasScrollableFrameMetrics(const WebRenderLayerScrollData* aLayer) const {
+ for (uint32_t i = 0; i < aLayer->GetScrollMetadataCount(); i++) {
+ if (aLayer->GetScrollMetadata(layers, i).GetMetrics().IsScrollable()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void SetScrollHandoff(WebRenderLayerScrollData* aChild,
+ WebRenderLayerScrollData* aParent) {
+ ModifyFrameMetrics(aChild, [&](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetScrollParentId(
+ aParent->GetScrollMetadata(layers, 0).GetMetrics().GetScrollId());
+ });
+ }
+
+ TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer) {
+ EXPECT_EQ(1u, aLayer->GetScrollMetadataCount());
+ return ApzcOf(aLayer, 0);
+ }
+
+ TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer,
+ uint32_t aIndex) {
+ EXPECT_LT(aIndex, aLayer->GetScrollMetadataCount());
+ // Unlike Layer, WebRenderLayerScrollData does not store the associated
+ // APZCs, so look it up using the tree manager instead.
+ RefPtr<AsyncPanZoomController> apzc = manager->GetTargetAPZC(
+ LayersId{0},
+ aLayer->GetScrollMetadata(layers, aIndex).GetMetrics().GetScrollId());
+ return (TestAsyncPanZoomController*)apzc.get();
+ }
+
+ void CreateSimpleScrollingLayer() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 200, 200),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+ }
+};
+
+#endif // mozilla_layers_APZCTreeManagerTester_h
diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.cpp b/gfx/layers/apz/test/gtest/APZTestAccess.cpp
new file mode 100644
index 0000000000..d55d7711f8
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestAccess.cpp
@@ -0,0 +1,27 @@
+/* -*- 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/. */
+
+#include "APZTestAccess.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+
+namespace mozilla {
+namespace layers {
+
+/*static*/
+void APZTestAccess::InitializeForTest(WebRenderLayerScrollData& aLayer,
+ int32_t aDescendantCount) {
+ aLayer.InitializeForTest(aDescendantCount);
+}
+
+/*static*/
+ScrollMetadata& APZTestAccess::GetScrollMetadataMut(
+ WebRenderLayerScrollData& aLayer, WebRenderScrollData& aOwner,
+ size_t aIndex) {
+ return aLayer.GetScrollMetadataMut(aOwner, aIndex);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.h b/gfx/layers/apz/test/gtest/APZTestAccess.h
new file mode 100644
index 0000000000..a56fb10a1a
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestAccess.h
@@ -0,0 +1,36 @@
+/* -*- 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_APZTestAccess_h
+#define mozilla_layers_APZTestAccess_h
+
+#include <cstddef> // for size_t
+#include <cstdint> // for int32_t
+
+namespace mozilla {
+namespace layers {
+
+struct ScrollMetadata;
+class WebRenderLayerScrollData;
+class WebRenderScrollData;
+
+// The only purpose of this class is to serve as a single type that can be
+// the target of a "friend class" declaration in APZ classes that want to
+// give APZ test code access to their private members.
+// APZ test code can then access those members via this class.
+class APZTestAccess {
+ public:
+ static void InitializeForTest(WebRenderLayerScrollData& aLayer,
+ int32_t aDescendantCount);
+ static ScrollMetadata& GetScrollMetadataMut(WebRenderLayerScrollData& aLayer,
+ WebRenderScrollData& aOwner,
+ size_t aIndex);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.cpp b/gfx/layers/apz/test/gtest/APZTestCommon.cpp
new file mode 100644
index 0000000000..bb47c4e274
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestCommon.cpp
@@ -0,0 +1,16 @@
+/* -*- 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/. */
+
+#include "APZTestCommon.h"
+
+already_AddRefed<AsyncPanZoomController> TestAPZCTreeManager::NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController) {
+ MockContentControllerDelayed* mcc =
+ static_cast<MockContentControllerDelayed*>(aController);
+ return MakeRefPtr<TestAsyncPanZoomController>(
+ aLayersId, mcc, this, AsyncPanZoomController::USE_GESTURE_DETECTOR)
+ .forget();
+}
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
diff --git a/gfx/layers/apz/test/gtest/InputUtils.h b/gfx/layers/apz/test/gtest/InputUtils.h
new file mode 100644
index 0000000000..16caec84f9
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/InputUtils.h
@@ -0,0 +1,151 @@
+/* -*- 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_InputUtils_h
+#define mozilla_layers_InputUtils_h
+
+/**
+ * Defines a set of utility functions for generating input events
+ * to an APZC/APZCTM during APZ gtests.
+ */
+
+#include "APZTestCommon.h"
+
+/* The InputReceiver template parameter used in the helper functions below needs
+ * to be a class that implements functions with the signatures:
+ * APZEventResult ReceiveInputEvent(const InputData& aEvent);
+ * void SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ * const nsTArray<uint32_t>& aBehaviours);
+ * The classes that currently implement these are APZCTreeManager and
+ * TestAsyncPanZoomController. Using this template allows us to test individual
+ * APZC instances in isolation and also an entire APZ tree, while using the same
+ * code to dispatch input events.
+ */
+
+template <class InputReceiver>
+void SetDefaultAllowedTouchBehavior(const RefPtr<InputReceiver>& aTarget,
+ uint64_t aInputBlockId,
+ int touchPoints = 1) {
+ nsTArray<uint32_t> defaultBehaviors;
+ // use the default value where everything is allowed
+ for (int i = 0; i < touchPoints; i++) {
+ defaultBehaviors.AppendElement(
+ mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN |
+ mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN |
+ mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM |
+ mozilla::layers::AllowedTouchBehavior::ANIMATING_ZOOM);
+ }
+ aTarget->SetAllowedTouchBehavior(aInputBlockId, defaultBehaviors);
+}
+
+inline MultiTouchInput CreateMultiTouchInput(
+ MultiTouchInput::MultiTouchType aType, TimeStamp aTime) {
+ return MultiTouchInput(aType, MillisecondsSinceStartup(aTime), aTime, 0);
+}
+
+template <class InputReceiver>
+APZEventResult TouchDown(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti);
+}
+
+template <class InputReceiver>
+APZEventResult TouchMove(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti);
+}
+
+template <class InputReceiver>
+APZEventResult TouchUp(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti);
+}
+
+template <class InputReceiver>
+APZEventResult Wheel(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, const ScreenPoint& aDelta,
+ TimeStamp aTime) {
+ ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, aPoint, aDelta.x,
+ aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+// Tests that use this function should set general.smoothScroll=true, otherwise
+// the smooth scroll animation code will set the animation duration to 0.
+template <class InputReceiver>
+APZEventResult SmoothWheel(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta, TimeStamp aTime) {
+ ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_SMOOTH,
+ ScrollWheelInput::SCROLLDELTA_LINE, aPoint, aDelta.x,
+ aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult MouseDown(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MouseInput input(MouseInput::MOUSE_DOWN,
+ MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime,
+ 0);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult MouseMove(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MouseInput input(MouseInput::MOUSE_MOVE,
+ MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime,
+ 0);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult MouseUp(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeStamp aTime) {
+ MouseInput input(MouseInput::MOUSE_UP, MouseInput::ButtonType::PRIMARY_BUTTON,
+ 0, 0, aPoint, aTime, 0);
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult PanGesture(PanGestureInput::PanGestureType aType,
+ const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta, TimeStamp aTime,
+ Modifiers aModifiers = MODIFIER_NONE,
+ bool aSimulateMomentum = false) {
+ PanGestureInput input(aType, aTime, aPoint, aDelta, aModifiers);
+ input.mSimulateMomentum = aSimulateMomentum;
+ if constexpr (std::is_same_v<InputReceiver, TestAsyncPanZoomController>) {
+ // In the case of TestAsyncPanZoomController we know for sure that the
+ // event will be handled by APZ so set it explicitly.
+ input.mHandledByAPZ = true;
+ }
+ return aTarget->ReceiveInputEvent(input);
+}
+
+template <class InputReceiver>
+APZEventResult PanGestureWithModifiers(PanGestureInput::PanGestureType aType,
+ Modifiers aModifiers,
+ const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta,
+ TimeStamp aTime) {
+ return PanGesture(aType, aTarget, aPoint, aDelta, aTime, aModifiers);
+}
+
+#endif // mozilla_layers_InputUtils_h
diff --git a/gfx/layers/apz/test/gtest/MockHitTester.cpp b/gfx/layers/apz/test/gtest/MockHitTester.cpp
new file mode 100644
index 0000000000..b445b02056
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/MockHitTester.cpp
@@ -0,0 +1,76 @@
+/* -*- 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/. */
+
+#include "MockHitTester.h"
+#include "TreeTraversal.h"
+#include "apz/src/APZCTreeManager.h"
+#include "apz/src/AsyncPanZoomController.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace mozilla::layers {
+
+IAPZHitTester::HitTestResult MockHitTester::GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ MOZ_ASSERT(!mQueuedResults.empty());
+ HitTestResult result = std::move(mQueuedResults.front());
+ mQueuedResults.pop();
+ return result;
+}
+
+void MockHitTester::QueueHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ gfx::CompositorHitTestInfo aHitInfo) {
+ LayersId layersId = GetRootLayersId(); // currently this is all the tests use
+ RefPtr<HitTestingTreeNode> node =
+ GetTargetNode(ScrollableLayerGuid(layersId, 0, aScrollId),
+ ScrollableLayerGuid::EqualsIgnoringPresShell);
+ MOZ_ASSERT(node);
+ AsyncPanZoomController* apzc = node->GetApzc();
+ MOZ_ASSERT(apzc);
+ HitTestResult result;
+ result.mTargetApzc = apzc;
+ result.mHitResult = aHitInfo;
+ result.mLayersId = layersId;
+ mQueuedResults.push(std::move(result));
+}
+
+void MockHitTester::QueueScrollbarThumbHitResult(
+ ScrollableLayerGuid::ViewID aScrollId, ScrollDirection aDirection) {
+ RecursiveMutexAutoLock lock(GetTreeLock());
+ LayersId layersId = GetRootLayersId(); // currently this is all the tests use
+ // First find the scrolalble node, to get the APZC.
+ RefPtr<HitTestingTreeNode> scrollableNode =
+ GetTargetNode(ScrollableLayerGuid(layersId, 0, aScrollId),
+ ScrollableLayerGuid::EqualsIgnoringPresShell);
+ MOZ_ASSERT(scrollableNode);
+ AsyncPanZoomController* apzc = scrollableNode->GetApzc();
+ MOZ_ASSERT(apzc);
+
+ // Now find the scroll thumb node.
+ RefPtr<HitTestingTreeNode> scrollThumbNode =
+ BreadthFirstSearch<ReverseIterator>(
+ GetRootNode(), [&](HitTestingTreeNode* aNode) {
+ return aNode->GetLayersId() == layersId &&
+ aNode->IsScrollThumbNode() &&
+ aNode->GetScrollbarDirection() == aDirection &&
+ aNode->GetScrollTargetId() == aScrollId;
+ });
+ MOZ_ASSERT(scrollThumbNode);
+
+ HitTestResult result;
+ result.mTargetApzc = apzc;
+ result.mHitResult = {gfx::CompositorHitTestFlags::eVisibleToHitTest,
+ gfx::CompositorHitTestFlags::eScrollbarThumb};
+ if (aDirection == ScrollDirection::eVertical) {
+ result.mHitResult += gfx::CompositorHitTestFlags::eScrollbarVertical;
+ }
+ InitializeHitTestingTreeNodeAutoLock(result.mScrollbarNode, lock,
+ scrollThumbNode);
+ mQueuedResults.push(std::move(result));
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/test/gtest/MockHitTester.h b/gfx/layers/apz/test/gtest/MockHitTester.h
new file mode 100644
index 0000000000..9f01ae8a2c
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/MockHitTester.h
@@ -0,0 +1,43 @@
+/* -*- 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_MockHitTester_h
+#define mozilla_layers_MockHitTester_h
+
+#include "apz/src/IAPZHitTester.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/layers/LayersTypes.h"
+
+#include <queue>
+
+namespace mozilla::layers {
+
+// IAPZHitTester implementation for APZ gtests.
+// This does not actually perform hit-testing, it just allows
+// the test code to specify the expected hit test results.
+class MockHitTester final : public IAPZHitTester {
+ public:
+ HitTestResult GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) override;
+
+ // Queue a hit test result whose target APZC is the APZC
+ // with scroll id |aScrollId|, and the provided hit test flags.
+ void QueueHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ gfx::CompositorHitTestInfo aHitInfo);
+
+ // Queue a hit test result whose target is the scrollbar of the APZC
+ // with scroll id |aScrollId| in the direction specified by |aDirection|.
+ void QueueScrollbarThumbHitResult(ScrollableLayerGuid::ViewID aScrollId,
+ ScrollDirection aDirection);
+
+ private:
+ std::queue<HitTestResult> mQueuedResults;
+};
+
+} // namespace mozilla::layers
+
+#endif // define mozilla_layers_MockHitTester_h
diff --git a/gfx/layers/apz/test/gtest/TestAxisLock.cpp b/gfx/layers/apz/test/gtest/TestAxisLock.cpp
new file mode 100644
index 0000000000..d1d13a9d52
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestAxisLock.cpp
@@ -0,0 +1,645 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+#include "gtest/gtest.h"
+
+#include <cmath>
+
+class APZCAxisLockCompatTester : public APZCTreeManagerTester,
+ public testing::WithParamInterface<int> {
+ public:
+ APZCAxisLockCompatTester() : oldAxisLockMode(0) { CreateMockHitTester(); }
+
+ int oldAxisLockMode;
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ RefPtr<TestAsyncPanZoomController> apzc;
+
+ void SetUp() {
+ APZCTreeManagerTester::SetUp();
+
+ oldAxisLockMode = Preferences::GetInt("apz.axis_lock.mode");
+
+ Preferences::SetInt("apz.axis_lock.mode", GetParam());
+ }
+
+ void TearDown() {
+ APZCTreeManagerTester::TearDown();
+
+ Preferences::SetInt("apz.axis_lock.mode", oldAxisLockMode);
+ }
+
+ static std::string PrintFromParam(const testing::TestParamInfo<int>& info) {
+ switch (info.param) {
+ case 0:
+ return "FREE";
+ case 1:
+ return "STANDARD";
+ case 2:
+ return "STICKY";
+ case 3:
+ return "DOMINANT_AXIS";
+ default:
+ return "UNKNOWN";
+ }
+ }
+};
+
+class APZCAxisLockTester : public APZCTreeManagerTester {
+ public:
+ APZCAxisLockTester() { CreateMockHitTester(); }
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ RefPtr<TestAsyncPanZoomController> apzc;
+
+ void SetupBasicTest() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+ }
+
+ void BreakStickyAxisLockTestGesture(const ScrollDirections& aDirections) {
+ float panX = 0;
+ float panY = 0;
+
+ if (aDirections.contains(ScrollDirection::eVertical)) {
+ panY = 30;
+ }
+ if (aDirections.contains(ScrollDirection::eHorizontal)) {
+ panX = 30;
+ }
+
+ // Kick off the gesture that may lock onto an axis
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(panX, panY), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(panX, panY), mcc->Time());
+ }
+
+ void BreakStickyAxisLockTest(const ScrollDirections& aDirections) {
+ // Create the gesture for the test.
+ BreakStickyAxisLockTestGesture(aDirections);
+
+ // Based on the scroll direction(s) ensure the state is what we expect.
+ if (aDirections == ScrollDirection::eVertical) {
+ apzc->AssertStateIsPanningLockedY();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+ } else if (aDirections == ScrollDirection::eHorizontal) {
+ apzc->AssertStateIsPanningLockedX();
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+ } else {
+ apzc->AssertStateIsPanning();
+ apzc->AssertNotAxisLocked();
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ }
+
+ // Cleanup for next test.
+ apzc->AdvanceAnimationsUntilEnd();
+ }
+};
+
+TEST_F(APZCAxisLockTester, BasicDominantAxisUse) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Kick off the initial gesture that triggers the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+
+ // Should be in a PANNING_LOCKED_Y state with no horizontal velocity.
+ apzc->AssertStateIsPanningLockedY();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have not panned on the horizontal axis.
+ ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_EQ(panEndOffset.x, 0);
+
+ // The lock onto the Y axis extends into momentum scroll.
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+
+ // Start the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // In momentum locking mode, we should still be locked onto the Y axis.
+ apzc->AssertStateIsPanMomentum();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time());
+
+ // After momentum scroll end, ensure we are no longer locked onto an axis.
+ apzc->AssertNotAxisLocked();
+
+ // Wait until the end of the animation and ensure the final state is
+ // reasonable.
+ apzc->AdvanceAnimationsUntilEnd();
+ ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+
+ // Ensure we have scrolled some amount on the Y axis in momentum scroll.
+ EXPECT_GT(finalOffset.y, panEndOffset.y);
+ EXPECT_EQ(finalOffset.x, 0.0f);
+}
+
+TEST_F(APZCAxisLockTester, NewGestureBreaksMomentumAxisLock) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Kick off the initial gesture that triggers the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(2, 1), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(30, 15), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(30, 15), mcc->Time());
+
+ // Should be in a PANNING_LOCKED_X state with no vertical velocity.
+ apzc->AssertStateIsPanningLockedX();
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Double check that we have not panned on the vertical axis.
+ ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_EQ(panEndOffset.y, 0);
+
+ // Ensure that the axis locks extends into momentum scroll.
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+
+ // Start the momentum scroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(80, 40), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // In momentum locking mode, we should still be locked onto the X axis.
+ apzc->AssertStateIsPanMomentum();
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+ EXPECT_GT(apzc->GetVelocityVector().x, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+
+ ParentLayerPoint beforeBreakOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_EQ(beforeBreakOffset.y, 0);
+ // Ensure we have scrolled some amount on the X axis in momentum scroll.
+ EXPECT_GT(beforeBreakOffset.x, panEndOffset.x);
+
+ // Kick off the gesture that breaks the lock onto the X axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ ParentLayerPoint afterBreakOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+
+ // The lock onto the X axis should be broken and we now should be locked
+ // onto the Y axis.
+ apzc->AssertStateIsPanningLockedY();
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+ EXPECT_EQ(apzc->GetVelocityVector().x, 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // The lock onto the Y axis extends into momentum scroll.
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+
+ // Wait until the end of the animation and ensure the final state is
+ // reasonable.
+ apzc->AdvanceAnimationsUntilEnd();
+ ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+
+ EXPECT_GT(finalOffset.y, 0);
+ // Ensure that we did not scroll on the X axis after the vertical scroll
+ // started.
+ EXPECT_EQ(finalOffset.x, afterBreakOffset.x);
+}
+
+TEST_F(APZCAxisLockTester, BreakStickyAxisLock) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 6.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Start a gesture to get us locked onto the Y axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the Y axis.
+ apzc->AssertStateIsPanningLockedY();
+
+ // Test switch to locking onto the X axis.
+ BreakStickyAxisLockTest(ScrollDirection::eHorizontal);
+
+ // Test switch back to locking onto the Y axis.
+ BreakStickyAxisLockTest(ScrollDirection::eVertical);
+
+ // Test breaking all axis locks from a Y axis lock.
+ BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal,
+ ScrollDirection::eVertical));
+
+ // We should be in a panning state.
+ apzc->AssertStateIsPanning();
+ apzc->AssertNotAxisLocked();
+
+ // Lock back to the X axis.
+ BreakStickyAxisLockTestGesture(ScrollDirection::eHorizontal);
+
+ // End the gesture.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Start a gesture to get us locked onto the X axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the X axis.
+ apzc->AssertStateIsPanningLockedX();
+
+ // Test breaking all axis locks from a X axis lock.
+ BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal,
+ ScrollDirection::eVertical));
+
+ // We should be in a panning state.
+ apzc->AssertStateIsPanning();
+ apzc->AssertNotAxisLocked();
+
+ // Test switch back to locking onto the Y axis.
+ BreakStickyAxisLockTest(ScrollDirection::eVertical);
+}
+
+TEST_F(APZCAxisLockTester, BreakAxisLockByLockAngle) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // Start a gesture to get us locked onto the Y axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the Y axis.
+ apzc->AssertStateIsPanningLockedY();
+
+ // Stay within 45 degrees from the X axis, and more than 22.5 degrees from
+ // the Y axis. This should break the Y lock and lock us to the X axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(12, 10), mcc->Time());
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have locked onto the X axis.
+ apzc->AssertStateIsPanningLockedX();
+
+ // End the gesture.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+}
+
+TEST_F(APZCAxisLockTester, TestDominantAxisScrolling) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 3);
+
+ int panY;
+ int panX;
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ ParentLayerPoint lastOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+
+ // In dominant axis mode, test pan gesture events with varying gesture
+ // angles and ensure that we only pan on one axis.
+ for (panX = 0, panY = 50; panY >= 0; panY -= 10, panX += 5) {
+ // Gesture that should be locked onto one axis
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager,
+ ScreenIntPoint(50, 50), ScreenIntPoint(panX, panY), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(static_cast<float>(panX), static_cast<float>(panY)),
+ mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ apzc->AdvanceAnimationsUntilEnd();
+
+ ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+
+ if (panX > panY) {
+ // If we're closer to the X axis ensure that we moved on the horizontal
+ // axis and there was no movement on the vertical axis.
+ EXPECT_GT(scrollOffset.x, lastOffset.x);
+ EXPECT_EQ(scrollOffset.y, lastOffset.y);
+ } else {
+ // If we're closer to the Y axis ensure that we moved on the vertical
+ // axis and there was no movement on the horizontal axis.
+ EXPECT_GT(scrollOffset.y, lastOffset.y);
+ EXPECT_EQ(scrollOffset.x, lastOffset.x);
+ }
+
+ lastOffset = scrollOffset;
+ }
+}
+
+TEST_F(APZCAxisLockTester, TestCanScrollWithAxisLock) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+
+ SetupBasicTest();
+
+ apzc = ApzcOf(root);
+
+ // The axis locks do not impact CanScroll()
+ apzc->SetAxisLocked(ScrollDirection::eHorizontal, true);
+ EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(10, 0)), true);
+
+ apzc->SetAxisLocked(ScrollDirection::eHorizontal, false);
+ apzc->SetAxisLocked(ScrollDirection::eVertical, true);
+ EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(0, 10)), true);
+}
+
+TEST_F(APZCAxisLockTester, TestScrollHandoffAxisLockConflict) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+
+ // Create two scrollable frames. One parent frame with one child.
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 500, 500));
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> rootApzc = ApzcOf(root);
+ apzc = ApzcOf(layers[1]);
+
+ // Create a gesture on the y-axis that should lock the x axis.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 15), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // We are locked onto the y-axis.
+ apzc->AssertAxisLocked(ScrollDirection::eVertical);
+
+ // There should be movement in the child.
+ ParentLayerPoint childCurrentOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_GT(childCurrentOffset.y, 0);
+ EXPECT_EQ(childCurrentOffset.x, 0);
+
+ // There should be no movement in the parent.
+ ParentLayerPoint parentCurrentOffset = rootApzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_EQ(parentCurrentOffset.y, 0);
+ EXPECT_EQ(parentCurrentOffset.x, 0);
+
+ // Create a gesture on the x-axis, that should be directed
+ // at the child, even if the x-axis is locked.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // We broke the y-axis lock and are now locked onto the x-axis.
+ apzc->AssertAxisLocked(ScrollDirection::eHorizontal);
+
+ // There should be some movement in the child on the x-axis.
+ ParentLayerPoint childFinalOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_GT(childFinalOffset.x, 0);
+
+ // There should still be no movement in the parent.
+ ParentLayerPoint parentFinalOffset = rootApzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+ EXPECT_EQ(parentFinalOffset.y, 0);
+ EXPECT_EQ(parentFinalOffset.x, 0);
+}
+
+// The delta from the initial pan gesture should be reflected in the
+// current offset for all axis locking modes.
+TEST_P(APZCAxisLockCompatTester, TestPanGestureStart) {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ apzc = ApzcOf(root);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimationsUntilEnd();
+ ParentLayerPoint currentOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling);
+
+ EXPECT_EQ(currentOffset.x, 0);
+ EXPECT_EQ(currentOffset.y, 10);
+}
+
+// All APZCAxisLockCompatTester tests should be run for each apz.axis_lock.mode.
+// If another mode is added, the value should be added to this list.
+INSTANTIATE_TEST_SUITE_P(APZCAxisLockCompat, APZCAxisLockCompatTester,
+ testing::Values(0, 1, 2, 3),
+ APZCAxisLockCompatTester::PrintFromParam);
diff --git a/gfx/layers/apz/test/gtest/TestBasic.cpp b/gfx/layers/apz/test/gtest/TestBasic.cpp
new file mode 100644
index 0000000000..8ac879ae5d
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestBasic.cpp
@@ -0,0 +1,791 @@
+/* -*- 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/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+
+static ScrollGenerationCounter sGenerationCounter;
+
+TEST_F(APZCBasicTester, Overzoom) {
+ // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 125, 150));
+ fm.SetVisualScrollOffset(CSSPoint(10, 0));
+ fm.SetZoom(CSSToParentLayerScale(1.0));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(0.8f, fm.GetZoom().scale);
+ // bug 936721 - PGO builds introduce rounding error so
+ // use a fuzzy match instead
+ EXPECT_LT(std::abs(fm.GetVisualScrollOffset().x), 1e-5);
+ EXPECT_LT(std::abs(fm.GetVisualScrollOffset().y), 1e-5);
+}
+
+TEST_F(APZCBasicTester, ZoomLimits) {
+ SCOPED_GFX_PREF_FLOAT("apz.min_zoom", 0.9f);
+ SCOPED_GFX_PREF_FLOAT("apz.max_zoom", 2.0f);
+
+ // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 125, 150));
+ fm.SetZoom(CSSToParentLayerScale(1.0));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ MakeApzcZoomable();
+
+ // This should take the zoom scale to 0.8, but we've capped it at 0.9.
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(0.9f, fm.GetZoom().scale);
+
+ // This should take the zoom scale to 2.7, but we've capped it at 2.
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 3, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(2.0f, fm.GetZoom().scale);
+}
+
+TEST_F(APZCBasicTester, SimpleTransform) {
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+}
+
+TEST_F(APZCBasicTester, ComplexTransform) {
+ // This test assumes there is a page that gets rendered to
+ // two layers. In CSS pixels, the first layer is 50x50 and
+ // the second layer is 25x50. The widget scale factor is 3.0
+ // and the presShell resolution is 2.0. Therefore, these layers
+ // end up being 300x300 and 150x300 in layer pixels.
+ //
+ // The second (child) layer has an additional CSS transform that
+ // stretches it by 2.0 on the x-axis. Therefore, after applying
+ // CSS transforms, the two layers are the same size in screen
+ // pixels.
+ //
+ // The screen itself is 24x24 in screen pixels (therefore 4x4 in
+ // CSS pixels). The displayport is 1 extra CSS pixel on all
+ // sides.
+
+ RefPtr<TestAsyncPanZoomController> childApzc =
+ new TestAsyncPanZoomController(LayersId{0}, mcc, tm);
+
+ const char* treeShape = "x(x)";
+ // LayerID 0 1
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 300, 300),
+ LayerIntRect(0, 0, 150, 300),
+ };
+ Matrix4x4 transforms[] = {
+ Matrix4x4(),
+ Matrix4x4(),
+ };
+ transforms[0].PostScale(
+ 0.5f, 0.5f,
+ 1.0f); // this results from the 2.0 resolution on the root layer
+ transforms[1].PostScale(
+ 2.0f, 1.0f,
+ 1.0f); // this is the 2.0 x-axis CSS transform on the child layer
+
+ auto layers = TestWRScrollData::Create(treeShape, *updater, layerVisibleRect,
+ transforms);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 24, 24));
+ metrics.SetDisplayPort(CSSRect(-1, -1, 6, 6));
+ metrics.SetVisualScrollOffset(CSSPoint(10, 10));
+ metrics.SetLayoutViewport(CSSRect(10, 10, 8, 8));
+ metrics.SetScrollableRect(CSSRect(0, 0, 50, 50));
+ metrics.SetCumulativeResolution(LayoutDeviceToLayerScale(2));
+ metrics.SetPresShellResolution(2.0f);
+ metrics.SetZoom(CSSToParentLayerScale(6));
+ metrics.SetDevPixelsPerCSSPixel(CSSToLayoutDeviceScale(3));
+ metrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID);
+
+ ScrollMetadata childMetadata = metadata;
+ FrameMetrics& childMetrics = childMetadata.GetMetrics();
+ childMetrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID + 1);
+
+ layers[0]->AppendScrollMetadata(layers, metadata);
+ layers[1]->AppendScrollMetadata(layers, childMetadata);
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // Both the parent and child layer should behave exactly the same here,
+ // because the CSS transform on the child layer does not affect the
+ // SampleContentTransformForFrame code
+
+ // initial transform
+ apzc->SetFrameMetrics(metrics);
+ apzc->NotifyLayersUpdated(metadata, true, true);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(60, 60), pointOut);
+
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->NotifyLayersUpdated(childMetadata, true, true);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(60, 60), pointOut);
+
+ // do an async scroll by 5 pixels and check the transform
+ metrics.ScrollBy(CSSPoint(5, 0));
+ apzc->SetFrameMetrics(metrics);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(90, 60), pointOut);
+
+ childMetrics.ScrollBy(CSSPoint(5, 0));
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(90, 60), pointOut);
+
+ // do an async zoom of 1.5x and check the transform
+ metrics.ZoomBy(1.5f);
+ apzc->SetFrameMetrics(metrics);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(135, 90), pointOut);
+
+ childMetrics.ZoomBy(1.5f);
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(
+ AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)),
+ viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(135, 90), pointOut);
+
+ childApzc->Destroy();
+}
+
+TEST_F(APZCBasicTester, Fling) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // Fling down. Each step scroll further down
+ Pan(apzc, touchStart, touchEnd);
+ ParentLayerPoint lastPoint;
+ for (int i = 1; i < 50; i += 1) {
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut,
+ TimeDuration::FromMilliseconds(1));
+ EXPECT_GT(pointOut.y, lastPoint.y);
+ lastPoint = pointOut;
+ }
+}
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+TEST_F(APZCBasicTester, ResumeInterruptedTouchDrag_Bug1592435) {
+ // Start a touch-drag and scroll some amount, not lifting the finger.
+ SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 1.0f / 1000.0f);
+ ScreenIntPoint touchPos(10, 50);
+ uint64_t touchBlock = TouchDown(apzc, touchPos, mcc->Time()).mInputBlockId;
+ SetDefaultAllowedTouchBehavior(apzc, touchBlock);
+ for (int i = 0; i < 20; ++i) {
+ touchPos.y -= 1;
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, touchPos, mcc->Time());
+ }
+
+ // Take note of the scroll offset before the interruption.
+ CSSPoint scrollOffsetBeforeInterruption =
+ apzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ // Have the main thread interrupt the touch-drag by sending
+ // a main thread scroll update to a nearby location.
+ CSSPoint mainThreadOffset = scrollOffsetBeforeInterruption;
+ mainThreadOffset.y -= 5;
+ ScrollMetadata metadata = apzc->GetScrollMetadata();
+ metadata.GetMetrics().SetLayoutScrollOffset(mainThreadOffset);
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(mainThreadOffset)));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metadata.GetMetrics().SetScrollGeneration(
+ scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Continue and finish the touch-drag gesture.
+ for (int i = 0; i < 20; ++i) {
+ touchPos.y -= 1;
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, touchPos, mcc->Time());
+ }
+
+ // Check that the portion of the touch-drag that occurred after
+ // the interruption caused additional scrolling.
+ CSSPoint finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset();
+ EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y);
+
+ // Now do the same thing, but for a visual scroll update.
+ scrollOffsetBeforeInterruption =
+ apzc->GetFrameMetrics().GetVisualScrollOffset();
+ mainThreadOffset = scrollOffsetBeforeInterruption;
+ mainThreadOffset.y -= 5;
+ metadata = apzc->GetScrollMetadata();
+ metadata.GetMetrics().SetVisualDestination(mainThreadOffset);
+ metadata.GetMetrics().SetScrollGeneration(
+ sGenerationCounter.NewMainThreadGeneration());
+ metadata.GetMetrics().SetVisualScrollUpdateType(FrameMetrics::eMainThread);
+ scrollUpdates.Clear();
+ metadata.SetScrollUpdates(scrollUpdates);
+ apzc->NotifyLayersUpdated(metadata, false, true);
+ for (int i = 0; i < 20; ++i) {
+ touchPos.y -= 1;
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, touchPos, mcc->Time());
+ }
+ finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset();
+ EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y);
+
+ // Clean up by ending the touch gesture.
+ mcc->AdvanceByMillis(1);
+ TouchUp(apzc, touchPos, mcc->Time());
+}
+#endif
+
+TEST_F(APZCBasicTester, RelativeScrollOffset) {
+ // Set up initial conditions: zoomed in, layout offset at (100, 100),
+ // visual offset at (120, 120); the relative offset is therefore (20, 20).
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 1000));
+ metrics.SetLayoutViewport(CSSRect(100, 100, 100, 100));
+ metrics.SetZoom(CSSToParentLayerScale(2.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(120, 120));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ // Scroll the layout viewport to (200, 200).
+ ScrollMetadata mainThreadMetadata = metadata;
+ FrameMetrics& mainThreadMetrics = mainThreadMetadata.GetMetrics();
+ mainThreadMetrics.SetLayoutScrollOffset(CSSPoint(200, 200));
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(200, 200))));
+ mainThreadMetadata.SetScrollUpdates(scrollUpdates);
+ mainThreadMetrics.SetScrollGeneration(
+ scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(mainThreadMetadata, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ // Check that the relative offset has been preserved.
+ metrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(metrics.GetLayoutScrollOffset(), CSSPoint(200, 200));
+ EXPECT_EQ(metrics.GetVisualScrollOffset(), CSSPoint(220, 220));
+}
+
+TEST_F(APZCBasicTester, MultipleSmoothScrollsSmooth) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ // We want to test that if we send multiple smooth scroll requests that we
+ // still smoothly animate, ie that we get non-zero change every frame while
+ // the animation is running.
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ // Structure of this test.
+ // -send a pure relative smooth scroll request via NotifyLayersUpdated
+ // -advance animations a few times, check that scroll offset is increasing
+ // after the first few advances
+ // -send a pure relative smooth scroll request via NotifyLayersUpdated
+ // -advance animations a few times, check that scroll offset is increasing
+ // -send a pure relative smooth scroll request via NotifyLayersUpdated
+ // -advance animations a few times, check that scroll offset is increasing
+
+ ScrollMetadata metadata2 = metadata;
+ nsTArray<ScrollPositionUpdate> scrollUpdates2;
+ scrollUpdates2.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll(
+ ScrollOrigin::Other, ScrollMode::Smooth,
+ CSSPoint::ToAppUnits(CSSPoint(0, 200))));
+ metadata2.SetScrollUpdates(scrollUpdates2);
+ metadata2.GetMetrics().SetScrollGeneration(
+ scrollUpdates2.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ // Get the animation going
+ for (uint32_t i = 0; i < 3; i++) {
+ SampleAnimationOneFrame();
+ }
+
+ float offset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing)
+ .y;
+ ASSERT_GT(offset, 0);
+ float lastOffset = offset;
+
+ for (uint32_t i = 0; i < 2; i++) {
+ for (uint32_t j = 0; j < 3; j++) {
+ SampleAnimationOneFrame();
+ offset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing)
+ .y;
+ ASSERT_GT(offset, lastOffset);
+ lastOffset = offset;
+ }
+
+ ScrollMetadata metadata3 = metadata;
+ nsTArray<ScrollPositionUpdate> scrollUpdates3;
+ scrollUpdates3.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll(
+ ScrollOrigin::Other, ScrollMode::Smooth,
+ CSSPoint::ToAppUnits(CSSPoint(0, 200))));
+ metadata3.SetScrollUpdates(scrollUpdates3);
+ metadata3.GetMetrics().SetScrollGeneration(
+ scrollUpdates3.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata3, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+ }
+
+ for (uint32_t j = 0; j < 7; j++) {
+ SampleAnimationOneFrame();
+ offset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing)
+ .y;
+ ASSERT_GT(offset, lastOffset);
+ lastOffset = offset;
+ }
+}
+
+class APZCSmoothScrollTester : public APZCBasicTester {
+ public:
+ // Test that a smooth scroll animation correctly handles its destination
+ // being updated by a relative scroll delta from the main thread (a "content
+ // shift").
+ void TestContentShift() {
+ // Set up scroll frame. Starting scroll position is (0, 0).
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ // Start smooth scroll via main-thread request.
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll(
+ ScrollOrigin::Other, ScrollMode::Smooth,
+ CSSPoint::ToAppUnits(CSSPoint(0, 1000))));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Sample the smooth scroll animation until we get past y=500.
+ apzc->AssertStateIsSmoothScroll();
+ float y = 0;
+ while (y < 500) {
+ SampleAnimationOneFrame();
+ y = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ }
+
+ // Send a relative scroll of y = -400.
+ scrollUpdates.Clear();
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll(
+ CSSPoint::ToAppUnits(CSSPoint(0, 500)),
+ CSSPoint::ToAppUnits(CSSPoint(0, 100))));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, false);
+
+ // Verify the relative scroll was applied but didn't cancel the animation.
+ float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(y2, y - 400);
+ apzc->AssertStateIsSmoothScroll();
+
+ // Sample the animation again and check that it respected the relative
+ // scroll.
+ SampleAnimationOneFrame();
+ float y3 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_GT(y3, y2);
+ ASSERT_LT(y3, 500);
+
+ // Continue animation until done and check that it ended up at a correctly
+ // adjusted destination.
+ apzc->AdvanceAnimationsUntilEnd();
+ float y4 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(y4, 600); // 1000 (initial destination) - 400 (relative scroll)
+ }
+
+ // Test that a smooth scroll animation correctly handles a content
+ // shift, followed by an UpdateDelta due to a new input event.
+ void TestContentShiftThenUpdateDelta() {
+ // Set up scroll frame. Starting position is (0, 0).
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 1000, 1000));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 1000, 1000));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetIsRootContent(true);
+ // Set the line scroll amount to 100 pixels. Note that SmoothWheel() takes
+ // a delta denominated in lines.
+ metadata.SetLineScrollAmount({100, 100});
+ // The page scroll amount also needs to be set, otherwise the wheel handling
+ // code will get confused by things like the "don't scroll more than one
+ // page" check.
+ metadata.SetPageScrollAmount({1000, 1000});
+ apzc->SetScrollMetadata(metadata);
+
+ // Send a wheel event to trigger smooth scrolling by 5 lines (= 500 pixels).
+ SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time());
+ apzc->AssertStateIsWheelScroll();
+
+ // Sample the wheel scroll animation until we get past y=200.
+ float y = 0;
+ while (y < 200) {
+ SampleAnimationOneFrame();
+ y = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ }
+
+ // Apply a content shift of y=100.
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll(
+ CSSPoint::ToAppUnits(CSSPoint(0, 200)),
+ CSSPoint::ToAppUnits(CSSPoint(0, 300))));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Check that the content shift was applied but didn't cancel the animation.
+ // At this point, the animation's internal state should be targeting a
+ // destination of y=600.
+ float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(y2, y + 100);
+ apzc->AssertStateIsWheelScroll();
+
+ // Sample the animation until we get past y=400.
+ while (y < 400) {
+ SampleAnimationOneFrame();
+ y = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ }
+
+ // Send another wheel event to trigger smooth scrolling by another 5 lines
+ // (=500 pixels). This should update the animation to target a destination
+ // of y=1100.
+ SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time());
+
+ // Continue the animation until done and check that it ended up at y=1100.
+ apzc->AdvanceAnimationsUntilEnd();
+ float yEnd = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(yEnd, 1100);
+ }
+
+ // Test that a content shift does not cause a smooth scroll animation to
+ // overshoot its (updated) destination.
+ void TestContentShiftDoesNotCauseOvershoot() {
+ // Follow the same steps as in TestContentShiftThenUpdateDelta(),
+ // except use a content shift of y=1000.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 1000, 1000));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 1000, 1000));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetIsRootContent(true);
+ metadata.SetLineScrollAmount({100, 100});
+ metadata.SetPageScrollAmount({1000, 1000});
+ apzc->SetScrollMetadata(metadata);
+
+ // First wheel event, smooth scroll destination is y=500.
+ SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time());
+ apzc->AssertStateIsWheelScroll();
+
+ // Sample until we get past y=200.
+ float y = 0;
+ while (y < 200) {
+ SampleAnimationOneFrame();
+ y = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ }
+
+ // Apply a content shift of y=1000. The current scroll position is now
+ // y>1200, and the updated destination is y=1500.
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll(
+ CSSPoint::ToAppUnits(CSSPoint(0, 200)),
+ CSSPoint::ToAppUnits(CSSPoint(0, 1200))));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, false, true);
+ float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ ASSERT_EQ(y2, y + 1000);
+ apzc->AssertStateIsWheelScroll();
+
+ // Sample until we get past y=1300.
+ while (y < 1300) {
+ SampleAnimationOneFrame();
+ y = apzc->GetFrameMetrics().GetVisualScrollOffset().y;
+ }
+
+ // Second wheel event, destination is now y=2000.
+ // MSD physics has a bug where the UpdateDelta() causes the content shift
+ // to be applied in duplicate on the next sample, causing the scroll
+ // position to be y>2000!
+ SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time());
+
+ // Check that the scroll position remains <= 2000 until the end of the
+ // animation.
+ while (apzc->IsWheelScrollAnimationRunning()) {
+ SampleAnimationOneFrame();
+ ASSERT_LE(apzc->GetFrameMetrics().GetVisualScrollOffset().y, 2000);
+ }
+ ASSERT_EQ(2000, apzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ }
+};
+
+TEST_F(APZCSmoothScrollTester, ContentShiftBezier) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false);
+ TestContentShift();
+}
+
+TEST_F(APZCSmoothScrollTester, ContentShiftMsd) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true);
+ TestContentShift();
+}
+
+TEST_F(APZCSmoothScrollTester, ContentShiftThenUpdateDeltaBezier) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false);
+ TestContentShiftThenUpdateDelta();
+}
+
+TEST_F(APZCSmoothScrollTester, ContentShiftThenUpdateDeltaMsd) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true);
+ TestContentShiftThenUpdateDelta();
+}
+
+TEST_F(APZCSmoothScrollTester, ContentShiftDoesNotCauseOvershootBezier) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false);
+ TestContentShiftDoesNotCauseOvershoot();
+}
+
+TEST_F(APZCSmoothScrollTester, ContentShiftDoesNotCauseOvershootMsd) {
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true);
+ TestContentShiftDoesNotCauseOvershoot();
+}
+
+TEST_F(APZCBasicTester, ZoomAndScrollableRectChangeAfterZoomChange) {
+ // We want to check that a small scrollable rect change (which causes us to
+ // reclamp our scroll position, including in the sampled state) does not move
+ // the scroll offset in the sample state based the zoom in the apzc, only
+ // based on the zoom in the sampled state.
+
+ // First we zoom in to the right hand side. Then start zooming out, then send
+ // a scrollable rect change and check that it doesn't change the sampled state
+ // scroll offset.
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ MakeApzcZoomable();
+
+ // Zoom to right side.
+ ZoomTarget zoomTarget{CSSRect(75, 25, 25, 25)};
+ apzc->ZoomToRect(zoomTarget, 0);
+
+ // Run the animation to completion, should take 250ms/16.67ms = 15 frames, but
+ // do extra to make sure.
+ for (uint32_t i = 0; i < 30; i++) {
+ SampleAnimationOneFrame();
+ }
+
+ EXPECT_FALSE(apzc->IsAsyncZooming());
+
+ // Zoom out.
+ ZoomTarget zoomTarget2{CSSRect(0, 0, 100, 100)};
+ apzc->ZoomToRect(zoomTarget2, 0);
+
+ // Run the animation a few times to get it going.
+ for (uint32_t i = 0; i < 2; i++) {
+ SampleAnimationOneFrame();
+ }
+
+ // Check that it is decreasing in scale.
+ float prevScale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ for (uint32_t i = 0; i < 2; i++) {
+ SampleAnimationOneFrame();
+ float scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ ASSERT_GT(prevScale, scale);
+ prevScale = scale;
+ }
+
+ float offset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing)
+ .x;
+
+ // Change the scrollable rect slightly to trigger a reclamp.
+ ScrollMetadata metadata2 = metadata;
+ metadata2.GetMetrics().SetScrollableRect(CSSRect(0, 0, 100, 1000.2));
+ apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ float newOffset =
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing)
+ .x;
+
+ ASSERT_EQ(newOffset, offset);
+}
+
+TEST_F(APZCBasicTester, ZoomToRectAndCompositionBoundsChange) {
+ // We want to check that content sending a composition bounds change (due to
+ // addition of scrollbars) during a zoom animation does not cause us to take
+ // the out of date content resolution.
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetCompositionBoundsWidthIgnoringScrollbars(ParentLayerCoord{100});
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ metrics.SetZoom(CSSToParentLayerScale(1.0));
+ metrics.SetIsRootContent(true);
+ apzc->SetFrameMetrics(metrics);
+
+ MakeApzcZoomable();
+
+ // Start a zoom to a rect.
+ ZoomTarget zoomTarget{CSSRect(25, 25, 25, 25)};
+ apzc->ZoomToRect(zoomTarget, 0);
+
+ // Run the animation a few times to get it going.
+ // Check that it is increasing in scale.
+ float prevScale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ for (uint32_t i = 0; i < 3; i++) {
+ SampleAnimationOneFrame();
+ float scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ ASSERT_GE(scale, prevScale);
+ prevScale = scale;
+ }
+
+ EXPECT_TRUE(apzc->IsAsyncZooming());
+
+ // Simulate the appearance of a scrollbar by reducing the width of
+ // the composition bounds, while keeping
+ // mCompositionBoundsWidthIgnoringScrollbars unchanged.
+ ScrollMetadata metadata2 = metadata;
+ metadata2.GetMetrics().SetCompositionBounds(ParentLayerRect(0, 0, 90, 100));
+ apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false,
+ /*thisLayerTreeUpdated=*/true);
+
+ float scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+
+ ASSERT_EQ(scale, prevScale);
+
+ // Run the rest of the animation to completion, should take 250ms/16.67ms = 15
+ // frames total, but do extra to make sure.
+ for (uint32_t i = 0; i < 30; i++) {
+ SampleAnimationOneFrame();
+ scale =
+ apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing)
+ .scale;
+ ASSERT_GE(scale, prevScale);
+ prevScale = scale;
+ }
+
+ EXPECT_FALSE(apzc->IsAsyncZooming());
+}
+
+TEST_F(APZCBasicTester, StartTolerance) {
+ SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 10 / tm->GetDPI());
+
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 100, 300));
+ fm.SetVisualScrollOffset(CSSPoint(0, 50));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ uint64_t touchBlock = TouchDown(apzc, {50, 50}, mcc->Time()).mInputBlockId;
+ SetDefaultAllowedTouchBehavior(apzc, touchBlock);
+
+ CSSPoint initialScrollOffset =
+ apzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, {50, 70}, mcc->Time());
+
+ // Expect 10 pixels of scrolling: the distance from (50,50) to (50,70)
+ // minus the 10-pixel touch start tolerance.
+ ASSERT_EQ(initialScrollOffset.y - 10,
+ apzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ mcc->AdvanceByMillis(1);
+ TouchMove(apzc, {50, 90}, mcc->Time());
+
+ // Expect 30 pixels of scrolling: the distance from (50,50) to (50,90)
+ // minus the 10-pixel touch start tolerance.
+ ASSERT_EQ(initialScrollOffset.y - 30,
+ apzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Clean up by ending the touch gesture.
+ mcc->AdvanceByMillis(1);
+ TouchUp(apzc, {50, 90}, mcc->Time());
+}
diff --git a/gfx/layers/apz/test/gtest/TestEventRegions.cpp b/gfx/layers/apz/test/gtest/TestEventRegions.cpp
new file mode 100644
index 0000000000..f0f262eb82
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestEventRegions.cpp
@@ -0,0 +1,201 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/layers/LayersTypes.h"
+
+class APZEventRegionsTester : public APZCTreeManagerTester {
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+
+ void CreateEventRegionsLayerTree1() {
+ const char* treeShape = "x(xx)";
+ LayerIntRect layerVisibleRects[] = {
+ LayerIntRect(0, 0, 200, 200), // root
+ LayerIntRect(0, 0, 100, 200), // left half
+ LayerIntRect(0, 100, 200, 100), // bottom half
+ };
+ CreateScrollData(treeShape, layerVisibleRects);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 2);
+ SetScrollHandoff(layers[1], root);
+ SetScrollHandoff(layers[2], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateEventRegionsLayerTree2() {
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRects[] = {
+ LayerIntRect(0, 0, 100, 500),
+ LayerIntRect(0, 150, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRects);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateBug1117712LayerTree() {
+ const char* treeShape = "x(x(x)x)";
+ // LayerID 0 1 2 3
+ // 0 is the root
+ // 1 is a container layer whose sole purpose to make a non-empty ancestor
+ // transform for 2, so that 2's screen-to-apzc and apzc-to-gecko
+ // transforms are different from 3's.
+ // 2 is a small layer that is the actual target
+ // 3 is a big layer obscuring 2 with a dispatch-to-content region
+ LayerIntRect layerVisibleRects[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 0, 0),
+ LayerIntRect(0, 0, 10, 10),
+ LayerIntRect(0, 0, 100, 100),
+ };
+ Matrix4x4 layerTransforms[] = {
+ Matrix4x4(),
+ Matrix4x4::Translation(50, 0, 0),
+ Matrix4x4(),
+ Matrix4x4(),
+ };
+ CreateScrollData(treeShape, layerVisibleRects, layerTransforms);
+
+ SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 10, 10));
+ SetScrollableFrameMetrics(layers[3],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[3], layers[2]);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ }
+};
+
+class APZEventRegionsTesterMock : public APZEventRegionsTester {
+ public:
+ APZEventRegionsTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZEventRegionsTesterMock, HitRegionImmediateResponse) {
+ CreateEventRegionsLayerTree1();
+
+ TestAsyncPanZoomController* root = ApzcOf(layers[0]);
+ TestAsyncPanZoomController* left = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* bottom = ApzcOf(layers[2]);
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on left"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on bottom"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, root->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on root"));
+ EXPECT_CALL(check, Call("Tap pending on d-t-c region"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on bottom again"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped on left this time"));
+ }
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ // Tap in the exposed hit regions of each of the layers once and ensure
+ // the clicks are dispatched right away
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Tap(manager, ScreenIntPoint(10, 10), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on left");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2);
+ Tap(manager, ScreenIntPoint(110, 110), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on bottom");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Tap(manager, ScreenIntPoint(110, 10), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on root");
+
+ // Now tap on the dispatch-to-content region where the layers overlap
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ Tap(manager, ScreenIntPoint(10, 110), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the main-thread timeout
+ check.Call("Tap pending on d-t-c region");
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on bottom again");
+
+ // Now let's do that again, but simulate a main-thread response
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ APZEventResult result =
+ Tap(manager, ScreenIntPoint(10, 110), tapDuration, nullptr);
+ nsTArray<ScrollableLayerGuid> targets;
+ targets.AppendElement(left->GetGuid());
+ manager->SetTargetAPZC(result.mInputBlockId, targets);
+ while (mcc->RunThroughDelayedTasks())
+ ; // this runs the tap event
+ check.Call("Tapped on left this time");
+}
+
+TEST_F(APZEventRegionsTesterMock, HitRegionAccumulatesChildren) {
+ CreateEventRegionsLayerTree2();
+
+ // Tap in the area of the child layer that's not directly included in the
+ // parent layer's hit region. Verify that it comes out of the APZC's
+ // content controller, which indicates the input events got routed correctly
+ // to the APZC.
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, _, rootApzc->GetGuid(), _, _))
+ .Times(1);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Tap(manager, ScreenIntPoint(10, 160), TimeDuration::FromMilliseconds(100));
+}
+
+TEST_F(APZEventRegionsTesterMock, Bug1117712) {
+ CreateBug1117712LayerTree();
+
+ TestAsyncPanZoomController* apzc2 = ApzcOf(layers[2]);
+
+ // These touch events should hit the dispatch-to-content region of layers[3]
+ // and so get queued with that APZC as the tentative target.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ APZEventResult result = Tap(manager, ScreenIntPoint(55, 5),
+ TimeDuration::FromMilliseconds(100), nullptr);
+ // But now we tell the APZ that really it hit layers[2], and expect the tap
+ // to be delivered at the correct coordinates.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(55, 5), 0,
+ apzc2->GetGuid(), _, _))
+ .Times(1);
+
+ nsTArray<ScrollableLayerGuid> targets;
+ targets.AppendElement(apzc2->GetGuid());
+ manager->SetTargetAPZC(result.mInputBlockId, targets);
+}
diff --git a/gfx/layers/apz/test/gtest/TestEventResult.cpp b/gfx/layers/apz/test/gtest/TestEventResult.cpp
new file mode 100644
index 0000000000..7e1d77bd84
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestEventResult.cpp
@@ -0,0 +1,476 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/LayersTypes.h"
+#include <tuple>
+
+class APZEventResultTester : public APZCTreeManagerTester {
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ void UpdateOverscrollBehavior(OverscrollBehavior aX, OverscrollBehavior aY) {
+ ModifyFrameMetrics(root, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) {
+ OverscrollBehaviorInfo overscroll;
+ overscroll.mBehaviorX = aX;
+ overscroll.mBehaviorY = aY;
+ sm.SetOverscrollBehavior(overscroll);
+ });
+ UpdateHitTestingTree();
+ }
+
+ void SetScrollOffsetOnMainThread(const CSSPoint& aPoint) {
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ ScrollMetadata metadata = apzc->GetScrollMetadata();
+ metadata.GetMetrics().SetLayoutScrollOffset(aPoint);
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(aPoint)));
+ metadata.SetScrollUpdates(scrollUpdates);
+ metadata.GetMetrics().SetScrollGeneration(
+ scrollUpdates.LastElement().GetGeneration());
+ apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false,
+ /*aThisLayerTreeUpdated=*/true);
+ }
+
+ void CreateScrollableRootLayer() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRects[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRects);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
+ metrics.SetIsRootContent(true);
+ });
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ }
+
+ enum class PreventDefaultFlag { No, Yes };
+ std::tuple<APZEventResult, APZHandledResult> TapDispatchToContent(
+ const ScreenIntPoint& aPoint, PreventDefaultFlag aPreventDefaultFlag) {
+ APZEventResult result =
+ Tap(manager, aPoint, TimeDuration::FromMilliseconds(100));
+
+ APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone,
+ ScrollDirections()};
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(
+ result.mInputBlockId, aPreventDefaultFlag == PreventDefaultFlag::Yes);
+ return {result, delayedAnswer};
+ }
+
+ void OverscrollDirectionsWithEventHandlerTest(
+ PreventDefaultFlag aPreventDefaultFlag) {
+ UpdateHitTestingTree();
+
+ APZHandledPlace expectedPlace =
+ aPreventDefaultFlag == PreventDefaultFlag::No
+ ? APZHandledPlace::HandledByRoot
+ : APZHandledPlace::HandledByContent;
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ EitherScrollDirection}));
+ }
+
+ // overscroll-behavior: contain, contain.
+ UpdateOverscrollBehavior(OverscrollBehavior::Contain,
+ OverscrollBehavior::Contain);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ ScrollDirections()}));
+ }
+
+ // overscroll-behavior: none, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::None,
+ OverscrollBehavior::None);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ ScrollDirections()}));
+ }
+
+ // overscroll-behavior: auto, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::Auto,
+ OverscrollBehavior::None);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ HorizontalScrollDirection}));
+ }
+
+ // overscroll-behavior: none, auto.
+ UpdateOverscrollBehavior(OverscrollBehavior::None,
+ OverscrollBehavior::Auto);
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ VerticalScrollDirection}));
+ }
+ }
+
+ void ScrollableDirectionsWithEventHandlerTest(
+ PreventDefaultFlag aPreventDefaultFlag) {
+ UpdateHitTestingTree();
+
+ APZHandledPlace expectedPlace =
+ aPreventDefaultFlag == PreventDefaultFlag::No
+ ? APZHandledPlace::HandledByRoot
+ : APZHandledPlace::HandledByContent;
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
+ EitherScrollDirection}));
+ }
+
+ // scroll down a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(0, 10));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(delayedHandledResult,
+ (APZHandledResult{
+ expectedPlace,
+ SideBits::eTop | SideBits::eBottom | SideBits::eRight,
+ EitherScrollDirection}));
+ }
+
+ // scroll to the bottom edge
+ SetScrollOffsetOnMainThread(CSSPoint(0, 100));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eRight | SideBits::eTop,
+ EitherScrollDirection}));
+ }
+
+ // scroll to right a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(10, 100));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace,
+ SideBits::eLeft | SideBits::eRight | SideBits::eTop,
+ EitherScrollDirection}));
+ }
+
+ // scroll to the right edge.
+ SetScrollOffsetOnMainThread(CSSPoint(100, 100));
+ {
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ auto [result, delayedHandledResult] =
+ TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ EXPECT_EQ(
+ delayedHandledResult,
+ (APZHandledResult{expectedPlace, SideBits::eTop | SideBits::eLeft,
+ EitherScrollDirection}));
+ }
+ }
+};
+
+TEST_F(APZEventResultTester, OverscrollDirections) {
+ CreateScrollableRootLayer();
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ // The default value of overscroll-behavior is auto.
+ APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ EitherScrollDirection);
+
+ // overscroll-behavior: contain, contain.
+ UpdateOverscrollBehavior(OverscrollBehavior::Contain,
+ OverscrollBehavior::Contain);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ ScrollDirections());
+
+ // overscroll-behavior: none, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::None);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ ScrollDirections());
+
+ // overscroll-behavior: auto, none.
+ UpdateOverscrollBehavior(OverscrollBehavior::Auto, OverscrollBehavior::None);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ HorizontalScrollDirection);
+
+ // overscroll-behavior: none, auto.
+ UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::Auto);
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
+ VerticalScrollDirection);
+}
+
+TEST_F(APZEventResultTester, ScrollableDirections) {
+ CreateScrollableRootLayer();
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ // scrollable to down/right.
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eBottom | SideBits::eRight);
+
+ // scroll down a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(0, 10));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ // also scrollable toward top.
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eTop | SideBits::eBottom | SideBits::eRight);
+
+ // scroll to the bottom edge
+ SetScrollOffsetOnMainThread(CSSPoint(0, 100));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eRight | SideBits::eTop);
+
+ // scroll to right a bit.
+ SetScrollOffsetOnMainThread(CSSPoint(10, 100));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eLeft | SideBits::eRight | SideBits::eTop);
+
+ // scroll to the right edge.
+ SetScrollOffsetOnMainThread(CSSPoint(100, 100));
+ result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
+ EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
+ SideBits::eLeft | SideBits::eTop);
+}
+
+class APZEventResultTesterMock : public APZEventResultTester {
+ public:
+ APZEventResultTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZEventResultTesterMock, OverscrollDirectionsWithEventHandler) {
+ CreateScrollableRootLayer();
+
+ OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::No);
+}
+
+TEST_F(APZEventResultTesterMock,
+ OverscrollDirectionsWithPreventDefaultEventHandler) {
+ CreateScrollableRootLayer();
+
+ OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes);
+}
+
+TEST_F(APZEventResultTesterMock, ScrollableDirectionsWithEventHandler) {
+ CreateScrollableRootLayer();
+
+ ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::No);
+}
+
+TEST_F(APZEventResultTesterMock,
+ ScrollableDirectionsWithPreventDefaultEventHandler) {
+ CreateScrollableRootLayer();
+
+ ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes);
+}
+
+// Test that APZEventResult::GetHandledResult() is correctly
+// populated.
+TEST_F(APZEventResultTesterMock, HandledByRootApzcFlag) {
+ // Create simple layer tree containing a dispatch-to-content region
+ // that covers part but not all of its area.
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRects[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRects);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
+ metrics.SetIsRootContent(true);
+ });
+ // away from the scrolling container layer.
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Tap the top half and check that we report that the event was
+ // handled by the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ APZEventResult result =
+ TouchDown(manager, ScreenIntPoint(50, 25), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 25), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(),
+ Some(APZHandledResult{APZHandledPlace::HandledByRoot,
+ SideBits::eBottom, EitherScrollDirection}));
+
+ // Tap the bottom half and check that we report that we're not
+ // sure whether the event was handled by the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+
+ // Register an input block callback that will tell us the
+ // delayed answer.
+ APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone,
+ ScrollDirections()};
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+
+ // Send APZ the relevant notifications to allow it to process the
+ // input block.
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/false);
+
+ // Check that we received the delayed answer and it is what we expect.
+ EXPECT_EQ(delayedAnswer,
+ (APZHandledResult{APZHandledPlace::HandledByRoot, SideBits::eBottom,
+ EitherScrollDirection}));
+
+ // Now repeat the tap on the bottom half, but simulate a prevent-default.
+ // This time, we expect a delayed answer of `HandledByContent`.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/true);
+ EXPECT_EQ(delayedAnswer,
+ (APZHandledResult{APZHandledPlace::HandledByContent,
+ SideBits::eBottom, EitherScrollDirection}));
+
+ // Shrink the scrollable area, now it's no longer scrollable.
+ ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 100));
+ });
+ UpdateHitTestingTree();
+ // Now repeat the tap on the bottom half with an event handler.
+ // This time, we expect a delayed answer of `Unhandled`.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetHandledResult(), Nothing());
+ manager->AddInputBlockCallback(
+ result.mInputBlockId,
+ {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) {
+ EXPECT_EQ(id, result.mInputBlockId);
+ delayedAnswer = answer;
+ }});
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/false);
+ EXPECT_EQ(delayedAnswer,
+ (APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone,
+ EitherScrollDirection}));
+
+ // Repeat the tap on the bottom half, with no event handler.
+ // Make sure we get an eager answer of `Unhandled`.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
+ TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
+ EXPECT_EQ(result.GetStatus(), nsEventStatus_eIgnore);
+ EXPECT_EQ(result.GetHandledResult(),
+ Some(APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone,
+ EitherScrollDirection}));
+}
diff --git a/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp
new file mode 100644
index 0000000000..08f7d3d2ac
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp
@@ -0,0 +1,252 @@
+/* -*- 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/. */
+
+#include <initializer_list>
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZCFlingAccelerationTester : public APZCTreeManagerTester {
+ protected:
+ void SetUp() {
+ APZCTreeManagerTester::SetUp();
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 800, 1000),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 800, 50000));
+ // Scroll somewhere into the middle of the scroll range, so that we have
+ // lots of space to scroll in both directions.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetVisualScrollUpdateType(
+ FrameMetrics::ScrollOffsetUpdateType::eMainThread);
+ aMetrics.SetVisualDestination(CSSPoint(0, 25000));
+ });
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ apzc = ApzcOf(root);
+ }
+
+ void ExecutePanGesture100Hz(const ScreenIntPoint& aStartPoint,
+ std::initializer_list<int32_t> aYDeltas) {
+ APZEventResult result = TouchDown(apzc, aStartPoint, mcc->Time());
+
+ // Allowed touch behaviours must be set after sending touch-start.
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ const TimeDuration kTouchTimeDelta100Hz =
+ TimeDuration::FromMilliseconds(10);
+
+ ScreenIntPoint currentLocation = aStartPoint;
+ for (int32_t delta : aYDeltas) {
+ mcc->AdvanceBy(kTouchTimeDelta100Hz);
+ if (delta != 0) {
+ currentLocation.y += delta;
+ Unused << TouchMove(apzc, currentLocation, mcc->Time());
+ }
+ }
+
+ Unused << TouchUp(apzc, currentLocation, mcc->Time());
+ }
+
+ void ExecuteWait(const TimeDuration& aDuration) {
+ TimeDuration remaining = aDuration;
+ const TimeDuration TIME_BETWEEN_FRAMES =
+ TimeDuration::FromSeconds(1) / int64_t(60);
+ while (remaining.ToMilliseconds() > 0) {
+ mcc->AdvanceBy(TIME_BETWEEN_FRAMES);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ remaining -= TIME_BETWEEN_FRAMES;
+ }
+ }
+
+ RefPtr<TestAsyncPanZoomController> apzc;
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+};
+
+enum class UpOrDown : uint8_t { Up, Down };
+
+// This is a macro so that the assertions print useful line numbers.
+#define CHECK_VELOCITY(aUpOrDown, aLowerBound, aUpperBound) \
+ do { \
+ auto vel = apzc->GetVelocityVector(); \
+ if (UpOrDown::aUpOrDown == UpOrDown::Up) { \
+ EXPECT_LT(vel.y, 0.0); \
+ } else { \
+ EXPECT_GT(vel.y, 0.0); \
+ } \
+ EXPECT_GE(vel.Length(), aLowerBound); \
+ EXPECT_LE(vel.Length(), aUpperBound); \
+ } while (0)
+
+// These tests have the following pattern: Two flings are executed, with a bit
+// of wait time in between. The deltas in each pan gesture have been captured
+// from a real phone, from touch events triggered by real fingers.
+// We check the velocity at the end to detect whether the fling was accelerated
+// or not. As an additional safety precaution, we also check the velocities for
+// the first fling, so that changes in behavior are easier to analyze.
+// One added challenge of this test is the fact that it has to work with on
+// multiple platforms, and we use different velocity estimation strategies and
+// different fling physics depending on the platform.
+// The upper and lower bounds for the velocities were chosen in such a way that
+// the test passes on all platforms. At the time of writing, we usually end up
+// with higher velocities on Android than on Desktop, so the observed velocities
+// on Android became the upper bounds and the observed velocities on Desktop
+// becaume the lower bounds, each rounded out to a multiple of 0.1.
+
+TEST_F(APZCFlingAccelerationTester, TwoNormalFlingsShouldAccelerate) {
+ ExecutePanGesture100Hz(ScreenIntPoint{665, 1244},
+ {0, 0, -21, -44, -52, -55, -53, -49, -46, -47});
+ CHECK_VELOCITY(Down, 4.5, 6.8);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(375));
+ CHECK_VELOCITY(Down, 2.2, 5.1);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{623, 1211},
+ {-6, -51, -55, 0, -53, -57, -60, -60, -56});
+ CHECK_VELOCITY(Down, 9.0, 14.0);
+}
+
+TEST_F(APZCFlingAccelerationTester, TwoFastFlingsShouldAccelerate) {
+ ExecutePanGesture100Hz(ScreenIntPoint{764, 714},
+ {9, 30, 49, 60, 64, 64, 62, 59, 51});
+ CHECK_VELOCITY(Up, 5.0, 7.5);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(447));
+ CHECK_VELOCITY(Up, 2.3, 5.2);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{743, 739},
+ {7, 0, 38, 66, 75, 146, 0, 119});
+ CHECK_VELOCITY(Up, 13.0, 20.0);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ FlingsInOppositeDirectionShouldNotAccelerate) {
+ ExecutePanGesture100Hz(ScreenIntPoint{728, 1381},
+ {0, 0, 0, -12, -24, -32, -43, -46, 0});
+ CHECK_VELOCITY(Down, 2.9, 5.3);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(153));
+ CHECK_VELOCITY(Down, 2.1, 4.8);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{698, 1059},
+ {0, 0, 14, 61, 41, 0, 45, 35});
+ CHECK_VELOCITY(Up, 3.2, 4.3);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ ShouldNotAccelerateWhenPreviousFlingHasSlowedDown) {
+ ExecutePanGesture100Hz(ScreenIntPoint{748, 1046},
+ {0, 9, 15, 23, 31, 30, 0, 34, 31, 29, 28, 24, 24, 11});
+ CHECK_VELOCITY(Up, 2.2, 3.0);
+ ExecuteWait(TimeDuration::FromMilliseconds(498));
+ CHECK_VELOCITY(Up, 0.5, 1.0);
+ ExecutePanGesture100Hz(ScreenIntPoint{745, 1056},
+ {0, 10, 17, 29, 29, 33, 33, 0, 31, 27, 13});
+ CHECK_VELOCITY(Up, 1.8, 2.7);
+}
+
+TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedAtStartOfPan) {
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{711, 1468},
+ {0, 0, 0, 0, -8, 0, -18, -32, -50, -57, -66, -68, -63, -60});
+ CHECK_VELOCITY(Down, 6.2, 8.6);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(285));
+ CHECK_VELOCITY(Down, 3.4, 7.4);
+
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{658, 1352},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, -8, -18, -34, -53, -70, -75, -75, -64});
+ CHECK_VELOCITY(Down, 6.7, 9.1);
+}
+
+TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedDuringPan) {
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{732, 1423},
+ {0, 0, 0, -5, 0, -15, -41, -71, -90, -93, -85, -64, -44});
+ CHECK_VELOCITY(Down, 7.5, 10.1);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(204));
+ CHECK_VELOCITY(Down, 4.8, 9.4);
+
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{651, 1372},
+ {0, 0, 0, -6, 0, -16, -26, -41, -49, -65, -66, -61, -50, -35, -24,
+ -17, -11, -8, -6, -5, -4, -3, -2, -2, -2, -2, -2, -2, -2, -2,
+ -3, -4, -5, -7, -9, -10, -10, -12, -18, -25, -23, -28, -30, -24});
+ CHECK_VELOCITY(Down, 2.5, 3.4);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ ShouldNotAccelerateWhenOppositeDirectionDuringPan) {
+ ExecutePanGesture100Hz(ScreenIntPoint{663, 1371},
+ {0, 0, 0, -5, -18, -31, -49, -56, -61, -54, -55});
+ CHECK_VELOCITY(Down, 5.4, 7.1);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(255));
+ CHECK_VELOCITY(Down, 3.1, 6.0);
+
+ ExecutePanGesture100Hz(
+ ScreenIntPoint{726, 930},
+ {0, 0, 0, 0, 30, 0, 19, 24, 32, 30, 37, 33,
+ 33, 32, 25, 23, 23, 18, 13, 9, 5, 3, 1, 0,
+ -7, -19, -38, -53, -68, -79, -85, -73, -64, -54});
+ CHECK_VELOCITY(Down, 7.0, 10.0);
+}
+
+TEST_F(APZCFlingAccelerationTester,
+ ShouldAccelerateAfterLongWaitIfVelocityStillHigh) {
+ // Reduce friction with the "Desktop" fling physics a little, so that it
+ // behaves more similarly to the Android fling physics, and has enough
+ // velocity after the wait time to allow for acceleration.
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.0012);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{739, 1424},
+ {0, 0, -5, -10, -20, 0, -110, -86, 0, -102, -105});
+ CHECK_VELOCITY(Down, 6.3, 9.4);
+
+ ExecuteWait(TimeDuration::FromMilliseconds(1117));
+ CHECK_VELOCITY(Down, 1.6, 3.3);
+
+ ExecutePanGesture100Hz(ScreenIntPoint{726, 1380},
+ {0, -8, 0, -30, -60, -87, -104, -111});
+ CHECK_VELOCITY(Down, 13.0, 23.0);
+}
+
+TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateAfterCanceledWithTap) {
+ // First, build up a lot of speed.
+ ExecutePanGesture100Hz(ScreenIntPoint{569, 710},
+ {11, 2, 107, 18, 148, 57, 133, 159, 21});
+ ExecuteWait(TimeDuration::FromMilliseconds(154));
+ ExecutePanGesture100Hz(ScreenIntPoint{581, 650},
+ {12, 68, 0, 162, 78, 140, 167});
+ ExecuteWait(TimeDuration::FromMilliseconds(123));
+ ExecutePanGesture100Hz(ScreenIntPoint{568, 723}, {11, 0, 79, 91, 131, 171});
+ ExecuteWait(TimeDuration::FromMilliseconds(123));
+ ExecutePanGesture100Hz(ScreenIntPoint{598, 678},
+ {8, 55, 22, 87, 117, 220, 54});
+ ExecuteWait(TimeDuration::FromMilliseconds(134));
+ ExecutePanGesture100Hz(ScreenIntPoint{585, 854}, {45, 137, 107, 102, 79});
+ ExecuteWait(TimeDuration::FromMilliseconds(246));
+
+ // Then, interrupt with a tap.
+ ExecutePanGesture100Hz(ScreenIntPoint{566, 812}, {0, 0, 0, 0});
+ ExecuteWait(TimeDuration::FromMilliseconds(869));
+
+ // Then do a regular fling.
+ ExecutePanGesture100Hz(ScreenIntPoint{599, 819},
+ {0, 0, 8, 35, 8, 38, 29, 37});
+
+ CHECK_VELOCITY(Up, 2.8, 4.2);
+}
diff --git a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
new file mode 100644
index 0000000000..f244ca4dc7
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
@@ -0,0 +1,845 @@
+/* -*- 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/. */
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+// Note: There are additional tests that test gesture detection behaviour
+// with multiple APZCs in TestTreeManager.cpp.
+
+class APZCGestureDetectorTester : public APZCBasicTester {
+ public:
+ APZCGestureDetectorTester()
+ : APZCBasicTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {}
+
+ protected:
+ FrameMetrics GetPinchableFrameMetrics() {
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(200, 200, 100, 200));
+ fm.SetScrollableRect(CSSRect(0, 0, 980, 1000));
+ fm.SetVisualScrollOffset(CSSPoint(300, 300));
+ fm.SetZoom(CSSToParentLayerScale(2.0));
+ // APZC only allows zooming on the root scrollable frame.
+ fm.SetIsRootContent(true);
+ // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100
+ return fm;
+ }
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCGestureDetectorTester, Pan_After_Pinch) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f);
+ SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ MakeApzcZoomable();
+
+ // Test parameters
+ float zoomAmount = 1.25;
+ float pinchLength = 100.0;
+ float pinchLengthScaled = pinchLength * zoomAmount;
+ int focusX = 250;
+ int focusY = 300;
+ int panDistance = 20;
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+ TimeDuration::FromMilliseconds(50);
+
+ int firstFingerId = 0;
+ int secondFingerId = firstFingerId + 1;
+
+ // Put fingers down
+ MultiTouchInput mti =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, focusX, focusY));
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, focusX, focusY));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Spread fingers out to enter the pinch state
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ firstFingerId, static_cast<int32_t>(focusX - pinchLength), focusY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ secondFingerId, static_cast<int32_t>(focusX + pinchLength), focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Do the actual pinch of 1.25x
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ secondFingerId, static_cast<int32_t>(focusX + pinchLengthScaled),
+ focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Verify that the zoom changed, just to make sure our code above did what it
+ // was supposed to.
+ FrameMetrics zoomedMetrics = apzc->GetFrameMetrics();
+ float newZoom = zoomedMetrics.GetZoom().scale;
+ EXPECT_EQ(originalMetrics.GetZoom().scale * zoomAmount, newZoom);
+
+ // Now we lift one finger...
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ secondFingerId, static_cast<int32_t>(focusX + pinchLengthScaled),
+ focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // ... and pan with the remaining finger. This pan just breaks through the
+ // distance threshold.
+ focusY += StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI();
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // This one does an actual pan of 20 pixels
+ focusY += panDistance;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Lift the remaining finger
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(
+ firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY));
+ apzc->ReceiveInputEvent(mti);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Verify that we scrolled
+ FrameMetrics finalMetrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(zoomedMetrics.GetVisualScrollOffset().y - (panDistance / newZoom),
+ finalMetrics.GetVisualScrollOffset().y);
+
+ // Clear out any remaining fling animation and pending tasks
+ apzc->AdvanceAnimationsUntilEnd();
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ apzc->AssertStateIsReset();
+}
+#endif
+
+TEST_F(APZCGestureDetectorTester, Pan_With_Tap) {
+ SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 0.1);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // Making the APZC zoomable isn't really needed for the correct operation of
+ // this test, but it could help catch regressions where we accidentally enter
+ // a pinch state.
+ MakeApzcZoomable();
+
+ // Test parameters
+ int touchX = 250;
+ int touchY = 300;
+ int panDistance = 20;
+
+ int firstFingerId = 0;
+ int secondFingerId = firstFingerId + 1;
+
+ const float panThreshold =
+ StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI();
+
+ // Put finger down
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ // Start a pan, break through the threshold
+ touchY += panThreshold;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Do an actual pan for a bit
+ touchY += panDistance;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Put a second finger down
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, touchX + 10, touchY));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ // Lift the second finger
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(secondFingerId, touchX + 10, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Bust through the threshold again
+ touchY += panThreshold;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Do some more actual panning
+ touchY += panDistance;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Lift the first finger
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(
+ CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti);
+
+ // Verify that we scrolled
+ FrameMetrics finalMetrics = apzc->GetFrameMetrics();
+ float zoom = finalMetrics.GetZoom().scale;
+ EXPECT_EQ(
+ originalMetrics.GetVisualScrollOffset().y - (panDistance * 2 / zoom),
+ finalMetrics.GetVisualScrollOffset().y);
+
+ // Clear out any remaining fling animation and pending tasks
+ apzc->AdvanceAnimationsUntilEnd();
+ while (mcc->RunThroughDelayedTasks())
+ ;
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, SecondTapIsFar_Bug1586496) {
+ // Test that we receive two single-tap events when two tap gestures are
+ // close in time but far in distance.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _, _))
+ .Times(2);
+
+ TimeDuration brief =
+ TimeDuration::FromMilliseconds(StaticPrefs::apz_max_tap_time() / 10.0);
+
+ ScreenIntPoint point(10, 10);
+ Tap(apzc, point, brief);
+
+ mcc->AdvanceBy(brief);
+
+ point.x += static_cast<int32_t>(apzc->GetSecondTapTolerance() * 2);
+ point.y += static_cast<int32_t>(apzc->GetSecondTapTolerance() * 2);
+
+ Tap(apzc, point, brief);
+}
+
+class APZCFlingStopTester : public APZCGestureDetectorTester {
+ protected:
+ // Start a fling, and then tap while the fling is ongoing. When
+ // aSlow is false, the tap will happen while the fling is at a
+ // high velocity, and we check that the tap doesn't trigger sending a tap
+ // to content. If aSlow is true, the tap will happen while the fling
+ // is at a slow velocity, and we check that the tap does trigger sending
+ // a tap to content. See bug 1022956.
+ void DoFlingStopTest(bool aSlow) {
+ int touchStart = 50;
+ int touchEnd = 10;
+
+ // Start the fling down.
+ Pan(apzc, touchStart, touchEnd);
+ // The touchstart from the pan will leave some cancelled tasks in the queue,
+ // clear them out
+
+ // If we want to tap while the fling is fast, let the fling advance for 10ms
+ // only. If we want the fling to slow down more, advance to 2000ms. These
+ // numbers may need adjusting if our friction and threshold values change,
+ // but they should be deterministic at least.
+ int timeDelta = aSlow ? 2000 : 10;
+ int tapCallsExpected = aSlow ? 2 : 1;
+
+ // Advance the fling animation by timeDelta milliseconds.
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(
+ &viewTransformOut, pointOut, TimeDuration::FromMilliseconds(timeDelta));
+
+ // Deliver a tap to abort the fling. Ensure that we get a SingleTap
+ // call out of it if and only if the fling is slow.
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _, _))
+ .Times(tapCallsExpected);
+ Tap(apzc, ScreenIntPoint(10, 10), 0);
+ while (mcc->RunThroughDelayedTasks())
+ ;
+
+ // Deliver another tap, to make sure that taps are flowing properly once
+ // the fling is aborted.
+ Tap(apzc, ScreenIntPoint(100, 100), 0);
+ while (mcc->RunThroughDelayedTasks())
+ ;
+
+ // Verify that we didn't advance any further after the fling was aborted, in
+ // either case.
+ ParentLayerPoint finalPointOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, finalPointOut);
+ EXPECT_EQ(pointOut.x, finalPointOut.x);
+ EXPECT_EQ(pointOut.y, finalPointOut.y);
+
+ apzc->AssertStateIsReset();
+ }
+
+ void DoFlingStopWithSlowListener(bool aPreventDefault) {
+ MakeApzcWaitForMainThread();
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ uint64_t blockId = 0;
+
+ // Start the fling down.
+ Pan(apzc, touchStart, touchEnd, PanOptions::None, nullptr, nullptr,
+ &blockId);
+ apzc->ConfirmTarget(blockId);
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ // Sample the fling a couple of times to ensure it's going.
+ ParentLayerPoint point, finalPoint;
+ AsyncTransform viewTransform;
+ apzc->SampleContentTransformForFrame(&viewTransform, point,
+ TimeDuration::FromMilliseconds(10));
+ apzc->SampleContentTransformForFrame(&viewTransform, finalPoint,
+ TimeDuration::FromMilliseconds(10));
+ EXPECT_GT(finalPoint.y, point.y);
+
+ // Now we put our finger down to stop the fling
+ blockId =
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()).mInputBlockId;
+
+ // Re-sample to make sure it hasn't moved
+ apzc->SampleContentTransformForFrame(&viewTransform, point,
+ TimeDuration::FromMilliseconds(10));
+ EXPECT_EQ(finalPoint.x, point.x);
+ EXPECT_EQ(finalPoint.y, point.y);
+
+ // respond to the touchdown that stopped the fling.
+ // even if we do a prevent-default on it, the animation should remain
+ // stopped.
+ apzc->ContentReceivedInputBlock(blockId, aPreventDefault);
+
+ // Verify the page hasn't moved
+ apzc->SampleContentTransformForFrame(&viewTransform, point,
+ TimeDuration::FromMilliseconds(70));
+ EXPECT_EQ(finalPoint.x, point.x);
+ EXPECT_EQ(finalPoint.y, point.y);
+
+ // clean up
+ TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCFlingStopTester, FlingStop) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopTest(false);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopTap) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopTest(true);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopSlowListener) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopWithSlowListener(false);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopPreventDefault) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ DoFlingStopWithSlowListener(true);
+}
+
+TEST_F(APZCGestureDetectorTester, ShortPress) {
+ MakeApzcUnzoomable();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ // This verifies that the single tap notification is sent after the
+ // touchup is fully processed. The ordering here is important.
+ EXPECT_CALL(check, Call("pre-tap"));
+ EXPECT_CALL(check, Call("post-tap"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10),
+ 0, apzc->GetGuid(), _, _))
+ .Times(1);
+ }
+
+ check.Call("pre-tap");
+ TapAndCheckStatus(apzc, ScreenIntPoint(10, 10),
+ TimeDuration::FromMilliseconds(100));
+ check.Call("post-tap");
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, MediumPress) {
+ MakeApzcUnzoomable();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ // This verifies that the single tap notification is sent after the
+ // touchup is fully processed. The ordering here is important.
+ EXPECT_CALL(check, Call("pre-tap"));
+ EXPECT_CALL(check, Call("post-tap"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10),
+ 0, apzc->GetGuid(), _, _))
+ .Times(1);
+ }
+
+ check.Call("pre-tap");
+ TapAndCheckStatus(apzc, ScreenIntPoint(10, 10),
+ TimeDuration::FromMilliseconds(400));
+ check.Call("post-tap");
+
+ apzc->AssertStateIsReset();
+}
+
+class APZCLongPressTester : public APZCGestureDetectorTester {
+ protected:
+ void DoLongPressTest(uint32_t aBehavior) {
+ MakeApzcUnzoomable();
+
+ APZEventResult result =
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+ uint64_t blockId = result.mInputBlockId;
+
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ // SetAllowedTouchBehavior() must be called after sending touch-start.
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+ apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors);
+ }
+ // Have content "respond" to the touchstart
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(check, Call("preHandleLongTap"));
+ blockId++;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10),
+ 0, apzc->GetGuid(), blockId, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTap"));
+
+ EXPECT_CALL(check, Call("preHandleLongTapUp"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eLongTapUp, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTapUp"));
+ }
+
+ // Manually invoke the longpress while the touch is currently down.
+ check.Call("preHandleLongTap");
+ mcc->RunThroughDelayedTasks();
+ check.Call("postHandleLongTap");
+
+ // Dispatching the longpress event starts a new touch block, which
+ // needs a new content response and also has a pending timeout task
+ // in the queue. Deal with those here. We do the content response first
+ // with preventDefault=false, and then we run the timeout task which
+ // "loses the race" and does nothing.
+ apzc->ContentReceivedInputBlock(blockId, false);
+ mcc->AdvanceByMillis(1000);
+
+ // Finally, simulate lifting the finger. Since the long-press wasn't
+ // prevent-defaulted, we should get a long-tap-up event.
+ check.Call("preHandleLongTapUp");
+ result = TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->RunThroughDelayedTasks();
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+ check.Call("postHandleLongTapUp");
+
+ apzc->AssertStateIsReset();
+ }
+
+ void DoLongPressPreventDefaultTest(uint32_t aBehavior) {
+ MakeApzcUnzoomable();
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+
+ int touchX = 10, touchStartY = 50, touchEndY = 10;
+
+ APZEventResult result =
+ TouchDown(apzc, ScreenIntPoint(touchX, touchStartY), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+ uint64_t blockId = result.mInputBlockId;
+
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ // SetAllowedTouchBehavior() must be called after sending touch-start.
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+ apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors);
+ }
+ // Have content "respond" to the touchstart
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(check, Call("preHandleLongTap"));
+ blockId++;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap,
+ LayoutDevicePoint(touchX, touchStartY), 0,
+ apzc->GetGuid(), blockId, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTap"));
+ }
+
+ // Manually invoke the longpress while the touch is currently down.
+ check.Call("preHandleLongTap");
+ mcc->RunThroughDelayedTasks();
+ check.Call("postHandleLongTap");
+
+ // There should be a TimeoutContentResponse task in the queue still,
+ // waiting for the response from the longtap event dispatched above.
+ // Send the signal that content has handled the long-tap, and then run
+ // the timeout task (it will be a no-op because the content "wins" the
+ // race. This takes the place of the "contextmenu" event.
+ apzc->ContentReceivedInputBlock(blockId, true);
+ mcc->AdvanceByMillis(1000);
+
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, touchX, touchEndY));
+ result = apzc->ReceiveInputEvent(mti);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap,
+ LayoutDevicePoint(touchX, touchEndY), 0,
+ apzc->GetGuid(), _, _))
+ .Times(0);
+ result = TouchUp(apzc, ScreenIntPoint(touchX, touchEndY), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus());
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCLongPressTester, LongPress) {
+ DoLongPressTest(kDefaultTouchBehavior);
+}
+
+TEST_F(APZCLongPressTester, LongPressPreventDefault) {
+ DoLongPressPreventDefaultTest(kDefaultTouchBehavior);
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTap) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(0);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], false);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapNotZoomable) {
+ MakeApzcWaitForMainThread();
+ MakeApzcUnzoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSecondTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], false);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultFirstOnly) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], true);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultBoth) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(0);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], true);
+ apzc->ContentReceivedInputBlock(blockIds[1], true);
+
+ apzc->AssertStateIsReset();
+}
+
+// Test for bug 947892
+// We test whether we dispatch tap event when the tap is followed by pinch.
+// Additionally test that the pinch gesture successfully results in zooming.
+TEST_F(APZCGestureDetectorTester, TapFollowedByPinch) {
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+
+ PinchWithTouchInput(
+ apzc, ScreenIntPoint(15, 15), 1.5,
+ PinchOptions().TimeBetweenTouchEvents(
+ // Time it so that the max tap timer expires while the fingers are
+ // down for the pinch but haven't started to move yet.
+ TimeDuration::FromMilliseconds(StaticPrefs::apz_max_tap_time() -
+ 90)));
+
+ EXPECT_GT(apzc->GetFrameMetrics().GetZoom().scale, 1.0f);
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, TapFollowedByMultipleTouches) {
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+
+ int inputId = 0;
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(
+ inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior}));
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20),
+ ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(
+ inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressInterruptedByWheel) {
+ // Since we try to allow concurrent input blocks of different types to
+ // co-exist, the wheel block shouldn't interrupt the long-press detection.
+ // But more importantly, this shouldn't crash, which is what it did at one
+ // point in time.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(1);
+
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ uint64_t touchBlockId = result.mInputBlockId;
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, touchBlockId);
+ }
+ mcc->AdvanceByMillis(10);
+ uint64_t wheelBlockId =
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time())
+ .mInputBlockId;
+ EXPECT_NE(touchBlockId, wheelBlockId);
+ mcc->AdvanceByMillis(1000);
+}
+
+TEST_F(APZCGestureDetectorTester, TapTimeoutInterruptedByWheel) {
+ // In this test, even though the wheel block comes right after the tap, the
+ // tap should still be dispatched because it completes fully before the wheel
+ // block arrived.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+
+ // We make the APZC zoomable so the gesture detector needs to wait to
+ // distinguish between tap and double-tap. During that timeout is when we
+ // insert the wheel event.
+ MakeApzcZoomable();
+
+ APZEventResult result = Tap(apzc, ScreenIntPoint(10, 10),
+ TimeDuration::FromMilliseconds(100), nullptr);
+ mcc->AdvanceByMillis(10);
+ uint64_t wheelBlockId =
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time())
+ .mInputBlockId;
+ EXPECT_NE(result.mInputBlockId, wheelBlockId);
+ while (mcc->RunThroughDelayedTasks())
+ ;
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay) {
+ // In this test, we ensure that any time spent waiting in the input queue for
+ // the content response is subtracted from the long-press timeout in the
+ // GestureEventListener. In this test the content response timeout is longer
+ // than the long-press timeout.
+ SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60);
+ SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30);
+
+ MakeApzcWaitForMainThread();
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("pre long-tap dispatch"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("post long-tap dispatch"));
+ }
+
+ // Touch down
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ uint64_t touchBlockId = result.mInputBlockId;
+ // Simulate content response after 10ms
+ mcc->AdvanceByMillis(10);
+ apzc->ContentReceivedInputBlock(touchBlockId, false);
+ apzc->SetAllowedTouchBehavior(touchBlockId, {kDefaultTouchBehavior});
+ apzc->ConfirmTarget(touchBlockId);
+ // Ensure long-tap event happens within 20ms after that
+ check.Call("pre long-tap dispatch");
+ mcc->AdvanceByMillis(20);
+ check.Call("post long-tap dispatch");
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay2) {
+ // Similar to the previous test, except this time we don't simulate the
+ // content response at all, and still expect the long-press to happen on
+ // schedule.
+ SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60);
+ SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30);
+
+ MakeApzcWaitForMainThread();
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("pre long-tap dispatch"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("post long-tap dispatch"));
+ }
+
+ // Touch down
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ // Ensure the long-tap happens within 30ms even though there's no content
+ // response.
+ check.Call("pre long-tap dispatch");
+ mcc->AdvanceByMillis(30);
+ check.Call("post long-tap dispatch");
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay3) {
+ // Similar to the previous test, except now we have the long-press delay
+ // being longer than the content response timeout.
+ SCOPED_GFX_PREF_INT("apz.content_response_timeout", 30);
+ SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 60);
+
+ MakeApzcWaitForMainThread();
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("pre long-tap dispatch"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0,
+ apzc->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("post long-tap dispatch"));
+ }
+
+ // Touch down
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ // Ensure the long-tap happens at the 60ms mark even though the input event
+ // waits in the input queue for the full content response timeout of 30ms
+ mcc->AdvanceByMillis(59);
+ check.Call("pre long-tap dispatch");
+ mcc->AdvanceByMillis(1);
+ check.Call("post long-tap dispatch");
+}
diff --git a/gfx/layers/apz/test/gtest/TestHitTesting.cpp b/gfx/layers/apz/test/gtest/TestHitTesting.cpp
new file mode 100644
index 0000000000..b0609d4f0a
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestHitTesting.cpp
@@ -0,0 +1,352 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+
+class APZHitTestingTester : public APZCTreeManagerTester {
+ protected:
+ ScreenToParentLayerMatrix4x4 transformToApzc;
+ ParentLayerToScreenMatrix4x4 transformToGecko;
+
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const ScreenPoint& aPoint) {
+ RefPtr<AsyncPanZoomController> hit =
+ manager->GetTargetAPZC(aPoint).mTargetApzc;
+ if (hit) {
+ transformToApzc = manager->GetScreenToApzcTransform(hit.get());
+ transformToGecko =
+ manager->GetApzcToGeckoTransform(hit.get(), LayoutAndVisual);
+ }
+ return hit.forget();
+ }
+
+ protected:
+ void DisableApzOn(WebRenderLayerScrollData* aLayer) {
+ ModifyFrameMetrics(aLayer, [](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetForceDisableApz(true);
+ });
+ }
+
+ void CreateComplexMultiLayerTree() {
+ const char* treeShape = "x(xx(x)xx(x(x)xx))";
+ // LayerID 0 12 3 45 6 7 89
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 300, 400), // root(0)
+ LayerIntRect(0, 0, 100, 100), // layer(1) in top-left
+ LayerIntRect(50, 50, 200, 300), // layer(2) centered in root(0)
+ LayerIntRect(50, 50, 200,
+ 300), // layer(3) fully occupying parent layer(2)
+ LayerIntRect(0, 200, 100, 100), // layer(4) in bottom-left
+ LayerIntRect(200, 0, 100,
+ 400), // layer(5) along the right 100px of root(0)
+ LayerIntRect(200, 0, 100, 200), // layer(6) taking up the top
+ // half of parent layer(5)
+ LayerIntRect(200, 0, 100,
+ 200), // layer(7) fully occupying parent layer(6)
+ LayerIntRect(200, 200, 100,
+ 100), // layer(8) in bottom-right (below (6))
+ LayerIntRect(200, 300, 100,
+ 100), // layer(9) in bottom-right (below (8))
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[4],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[6],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[7],
+ ScrollableLayerGuid::START_SCROLL_ID + 2);
+ SetScrollableFrameMetrics(layers[8],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[9],
+ ScrollableLayerGuid::START_SCROLL_ID + 3);
+ }
+
+ void CreateBug1148350LayerTree() {
+ const char* treeShape = "x(x)";
+ // LayerID 0 1
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 200, 200),
+ LayerIntRect(0, 0, 200, 200),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
+ }
+};
+
+TEST_F(APZHitTestingTester, ComplexMultiLayerTree) {
+ CreateComplexMultiLayerTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ /* The layer tree looks like this:
+
+ 0
+ |----|--+--|----|
+ 1 2 4 5
+ | /|\
+ 3 6 8 9
+ |
+ 7
+
+ Layers 1,2 have the same APZC
+ Layers 4,6,8 have the same APZC
+ Layer 7 has an APZC
+ Layer 9 has an APZC
+ */
+
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ // Ensure all the scrollable layers have an APZC
+
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[0]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[3]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[4]));
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[5]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[6]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[7]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[8]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[9]));
+ // Ensure those that scroll together have the same APZCs
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+ EXPECT_EQ(ApzcOf(layers[4]), ApzcOf(layers[6]));
+ EXPECT_EQ(ApzcOf(layers[8]), ApzcOf(layers[6]));
+ // Ensure those that don't scroll together have different APZCs
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[4]));
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[7]));
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[9]));
+ EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[7]));
+ EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[9]));
+ EXPECT_NE(ApzcOf(layers[7]), ApzcOf(layers[9]));
+ // Ensure the APZC parent chains are set up correctly
+ TestAsyncPanZoomController* layers1_2 = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* layers4_6_8 = ApzcOf(layers[4]);
+ TestAsyncPanZoomController* layer7 = ApzcOf(layers[7]);
+ TestAsyncPanZoomController* layer9 = ApzcOf(layers[9]);
+ EXPECT_EQ(nullptr, layers1_2->GetParent());
+ EXPECT_EQ(nullptr, layers4_6_8->GetParent());
+ EXPECT_EQ(layers4_6_8, layer7->GetParent());
+ EXPECT_EQ(nullptr, layer9->GetParent());
+ // Ensure the hit-testing tree looks like the layer tree
+ RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
+ RefPtr<HitTestingTreeNode> node5 = root->GetLastChild();
+ RefPtr<HitTestingTreeNode> node4 = node5->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node2 = node4->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node1 = node2->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node3 = node2->GetLastChild();
+ RefPtr<HitTestingTreeNode> node9 = node5->GetLastChild();
+ RefPtr<HitTestingTreeNode> node8 = node9->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node6 = node8->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node7 = node6->GetLastChild();
+ EXPECT_EQ(nullptr, node1->GetPrevSibling());
+ EXPECT_EQ(nullptr, node3->GetPrevSibling());
+ EXPECT_EQ(nullptr, node6->GetPrevSibling());
+ EXPECT_EQ(nullptr, node7->GetPrevSibling());
+ EXPECT_EQ(nullptr, node1->GetLastChild());
+ EXPECT_EQ(nullptr, node3->GetLastChild());
+ EXPECT_EQ(nullptr, node4->GetLastChild());
+ EXPECT_EQ(nullptr, node7->GetLastChild());
+ EXPECT_EQ(nullptr, node8->GetLastChild());
+ EXPECT_EQ(nullptr, node9->GetLastChild());
+
+ // Assertions about hit-testing have been ported to mochitest,
+ // in helper_hittest_bug1730606-4.html.
+}
+
+TEST_F(APZHitTestingTester, TestRepaintFlushOnNewInputBlock) {
+ // The main purpose of this test is to verify that touch-start events (or
+ // anything that starts a new input block) don't ever get untransformed. This
+ // should always hold because the APZ code should flush repaints when we start
+ // a new input block and the transform to gecko space should be empty.
+
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ RefPtr<TestAsyncPanZoomController> apzcroot = ApzcOf(root);
+
+ // At this point, the following holds (all coordinates in screen pixels):
+ // layers[0] has content from (0,0)-(500,500), clipped by composition bounds
+ // (0,0)-(200,200)
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-first-touch-start"));
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-second-fling"));
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-second-touch-start"));
+ }
+
+ // This first pan will move the APZC by 50 pixels, and dispatch a paint
+ // request.
+ Pan(apzcroot, 100, 50, PanOptions::NoFling);
+
+ // Verify that a touch start doesn't get untransformed
+ ScreenIntPoint touchPoint(50, 50);
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ SingleTouchData(0, touchPoint, ScreenSize(0, 0), 0, 0));
+
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(mti).GetStatus());
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+ check.Call("post-first-touch-start");
+
+ // Send a touchend to clear state
+ mti.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(mti);
+
+ mcc->AdvanceByMillis(1000);
+
+ // Now do two pans. The first of these will dispatch a repaint request, as
+ // above. The second will get stuck in the paint throttler because the first
+ // one doesn't get marked as "completed", so this will result in a non-empty
+ // LD transform. (Note that any outstanding repaint requests from the first
+ // half of this test don't impact this half because we advance the time by 1
+ // second, which will trigger the max-wait-exceeded codepath in the paint
+ // throttler).
+ Pan(apzcroot, 100, 50, PanOptions::NoFling);
+ check.Call("post-second-fling");
+ Pan(apzcroot, 100, 50, PanOptions::NoFling);
+
+ // Ensure that a touch start again doesn't get untransformed by flushing
+ // a repaint
+ mti.mType = MultiTouchInput::MULTITOUCH_START;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(mti).GetStatus());
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+ check.Call("post-second-touch-start");
+
+ mti.mType = MultiTouchInput::MULTITOUCH_END;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(mti).GetStatus());
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+}
+
+TEST_F(APZHitTestingTester, TestRepaintFlushOnWheelEvents) {
+ // The purpose of this test is to ensure that wheel events trigger a repaint
+ // flush as per bug 1166871, and that the wheel event untransform is a no-op.
+
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(3));
+ ScreenPoint origin(100, 50);
+ for (int i = 0; i < 3; i++) {
+ ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(swi).GetStatus());
+ EXPECT_EQ(origin, swi.mOrigin);
+
+ AsyncTransform viewTransform;
+ ParentLayerPoint point;
+ apzcroot->SampleContentTransformForFrame(&viewTransform, point);
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ((i + 1) * 10, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ((i + 1) * -10, viewTransform.mTranslation.y);
+
+ mcc->AdvanceByMillis(5);
+ }
+}
+
+TEST_F(APZHitTestingTester, TestForceDisableApz) {
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ DisableApzOn(root);
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ ScreenPoint origin(100, 50);
+ ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(swi).GetStatus());
+ EXPECT_EQ(origin, swi.mOrigin);
+
+ AsyncTransform viewTransform;
+ ParentLayerPoint point;
+ apzcroot->SampleContentTransformForFrame(&viewTransform, point);
+ // Since APZ is force-disabled, we expect to see the async transform via
+ // the NORMAL AsyncMode, but not via the RESPECT_FORCE_DISABLE AsyncMode.
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ(10, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ(-10, viewTransform.mTranslation.y);
+ viewTransform = apzcroot->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForCompositing);
+ point = apzcroot->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing);
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ(0, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ(0, viewTransform.mTranslation.y);
+
+ mcc->AdvanceByMillis(10);
+
+ // With untransforming events we should get normal behaviour (in this case,
+ // no noticeable untransform, because the repaint request already got
+ // flushed).
+ swi = ScrollWheelInput(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 0,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault,
+ manager->ReceiveInputEvent(swi).GetStatus());
+ EXPECT_EQ(origin, swi.mOrigin);
+}
+
+TEST_F(APZHitTestingTester, Bug1148350) {
+ CreateBug1148350LayerTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0,
+ ApzcOf(layers[1])->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped without transform"));
+ EXPECT_CALL(*mcc,
+ HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0,
+ ApzcOf(layers[1])->GetGuid(), _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Tapped with interleaved transform"));
+ }
+
+ Tap(manager, ScreenIntPoint(100, 100), TimeDuration::FromMilliseconds(100));
+ mcc->RunThroughDelayedTasks();
+ check.Call("Tapped without transform");
+
+ uint64_t blockId =
+ TouchDown(manager, ScreenIntPoint(100, 100), mcc->Time()).mInputBlockId;
+ SetDefaultAllowedTouchBehavior(manager, blockId);
+ mcc->AdvanceByMillis(100);
+
+ layers[0]->SetVisibleRect(LayerIntRect(0, 50, 200, 150));
+ layers[0]->SetTransform(Matrix4x4::Translation(0, 50, 0));
+ UpdateHitTestingTree();
+
+ TouchUp(manager, ScreenIntPoint(100, 100), mcc->Time());
+ mcc->RunThroughDelayedTasks();
+ check.Call("Tapped with interleaved transform");
+}
diff --git a/gfx/layers/apz/test/gtest/TestInputQueue.cpp b/gfx/layers/apz/test/gtest/TestInputQueue.cpp
new file mode 100644
index 0000000000..18d1c00a2d
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestInputQueue.cpp
@@ -0,0 +1,48 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+// Test of scenario described in bug 1269067 - that a continuing mouse drag
+// doesn't interrupt a wheel scrolling animation
+TEST_F(APZCTreeManagerTester, WheelInterruptedByMouseDrag) {
+ // Needed because the test uses SmoothWheel()
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+
+ // Set up a scrollable layer
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // First start the mouse drag
+ uint64_t dragBlockId =
+ MouseDown(apzc, ScreenIntPoint(5, 5), mcc->Time()).mInputBlockId;
+ uint64_t tmpBlockId =
+ MouseMove(apzc, ScreenIntPoint(6, 6), mcc->Time()).mInputBlockId;
+ EXPECT_EQ(dragBlockId, tmpBlockId);
+
+ // Insert the wheel event, check that it has a new block id
+ uint64_t wheelBlockId =
+ SmoothWheel(apzc, ScreenIntPoint(6, 6), ScreenPoint(0, 1), mcc->Time())
+ .mInputBlockId;
+ EXPECT_NE(dragBlockId, wheelBlockId);
+
+ // Continue the drag, check that the block id is the same as before
+ tmpBlockId = MouseMove(apzc, ScreenIntPoint(7, 5), mcc->Time()).mInputBlockId;
+ EXPECT_EQ(dragBlockId, tmpBlockId);
+
+ // Finish the wheel animation
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // Check that it scrolled
+ ParentLayerPoint scroll = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ EXPECT_EQ(scroll.x, 0);
+ EXPECT_EQ(scroll.y, 10); // We scrolled 1 "line" or 10 pixels
+}
diff --git a/gfx/layers/apz/test/gtest/TestOverscroll.cpp b/gfx/layers/apz/test/gtest/TestOverscroll.cpp
new file mode 100644
index 0000000000..7cfd8d10e1
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestOverscroll.cpp
@@ -0,0 +1,2029 @@
+/* -*- 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/. */
+
+#include "APZCBasicTester.h"
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+
+#include "InputUtils.h"
+
+class APZCOverscrollTester : public APZCBasicTester {
+ public:
+ explicit APZCOverscrollTester(
+ AsyncPanZoomController::GestureBehavior aGestureBehavior =
+ AsyncPanZoomController::DEFAULT_GESTURES)
+ : APZCBasicTester(aGestureBehavior) {}
+
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+
+ void TestOverscroll() {
+ // Pan sufficiently to hit overscroll behavior
+ PanIntoOverscroll();
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+ }
+
+ void PanIntoOverscroll() {
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ }
+
+ /**
+ * Sample animations until we recover from overscroll.
+ * @param aExpectedScrollOffset the expected reported scroll offset
+ * throughout the animation
+ */
+ void SampleAnimationUntilRecoveredFromOverscroll(
+ const ParentLayerPoint& aExpectedScrollOffset) {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ bool recoveredFromOverscroll = false;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ while (apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut)) {
+ // The reported scroll offset should be the same throughout.
+ EXPECT_EQ(aExpectedScrollOffset, pointOut);
+
+ // Trigger computation of the overscroll tranform, to make sure
+ // no assetions fire during the calculation.
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+
+ if (!apzc->IsOverscrolled()) {
+ recoveredFromOverscroll = true;
+ }
+
+ mcc->AdvanceBy(increment);
+ }
+ EXPECT_TRUE(recoveredFromOverscroll);
+ apzc->AssertStateIsReset();
+ }
+
+ ScrollableLayerGuid CreateSimpleRootScrollableForWebRender() {
+ ScrollableLayerGuid guid;
+ guid.mScrollId = ScrollableLayerGuid::START_SCROLL_ID;
+ guid.mLayersId = LayersId{0};
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetScrollId(guid.mScrollId);
+ metadata.SetIsLayersIdRoot(true);
+
+ WebRenderLayerScrollData rootLayerScrollData;
+ rootLayerScrollData.InitializeRoot(0);
+ WebRenderScrollData scrollData;
+ rootLayerScrollData.AppendScrollMetadata(scrollData, metadata);
+ scrollData.AddLayerData(std::move(rootLayerScrollData));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(guid.mLayersId, mcc);
+ tm->UpdateHitTestingTree(WebRenderScrollDataWrapper(*updater, &scrollData),
+ false, guid.mLayersId, 0);
+ return guid;
+ }
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, FlingIntoOverscroll) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ // Scroll down by 25 px. Don't fling for simplicity.
+ Pan(apzc, 50, 25, PanOptions::NoFling);
+
+ // Now scroll back up by 20px, this time flinging after.
+ // The fling should cover the remaining 5 px of room to scroll, then
+ // go into overscroll, and finally snap-back to recover from overscroll.
+ Pan(apzc, 25, 45);
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ bool reachedOverscroll = false;
+ bool recoveredFromOverscroll = false;
+ while (apzc->AdvanceAnimations(mcc->GetSampleTime())) {
+ if (!reachedOverscroll && apzc->IsOverscrolled()) {
+ reachedOverscroll = true;
+ }
+ if (reachedOverscroll && !apzc->IsOverscrolled()) {
+ recoveredFromOverscroll = true;
+ }
+ mcc->AdvanceBy(increment);
+ }
+ EXPECT_TRUE(reachedOverscroll);
+ EXPECT_TRUE(recoveredFromOverscroll);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverScrollPanning) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ TestOverscroll();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Tests that an overscroll animation doesn't trigger an assertion failure
+// in the case where a sample has a velocity of zero.
+TEST_F(APZCOverscrollTester, OverScroll_Bug1152051a) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Doctor the prefs to make the velocity zero at the end of the first sample.
+
+ // This ensures our incoming velocity to the overscroll animation is
+ // a round(ish) number, 4.9 (that being the distance of the pan before
+ // overscroll, which is 500 - 10 = 490 pixels, divided by the duration of
+ // the pan, which is 100 ms).
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0);
+
+ TestOverscroll();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Tests that ending an overscroll animation doesn't leave around state that
+// confuses the next overscroll animation.
+TEST_F(APZCOverscrollTester, OverScroll_Bug1152051b) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.overscroll.stop_distance_threshold", 0.1f);
+
+ // Pan sufficiently to hit overscroll behavior
+ PanIntoOverscroll();
+
+ // Sample animations once, to give the fling animation started on touch-up
+ // a chance to realize it's overscrolled, and schedule a call to
+ // HandleFlingOverscroll().
+ SampleAnimationOnce();
+
+ // This advances the time and runs the HandleFlingOverscroll task scheduled in
+ // the previous call, which starts an overscroll animation. It then samples
+ // the overscroll animation once, to get it to initialize the first overscroll
+ // sample.
+ SampleAnimationOnce();
+
+ // Do a touch-down to cancel the overscroll animation, and then a touch-up
+ // to schedule a new one since we're still overscrolled. We don't pan because
+ // panning can trigger functions that clear the overscroll animation state
+ // in other ways.
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+ TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+
+ // Sample the second overscroll animation to its end.
+ // If the ending of the first overscroll animation fails to clear state
+ // properly, this will assert.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Tests that the page doesn't get stuck in an
+// overscroll animation after a low-velocity pan.
+TEST_F(APZCOverscrollTester, OverScrollAfterLowVelocityPan_Bug1343775) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Pan into overscroll with a velocity less than the
+ // apz.fling_min_velocity_threshold preference.
+ Pan(apzc, 10, 30);
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // Check that we recovered from overscroll.
+ EXPECT_FALSE(apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverScrollAbort) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Pan sufficiently to hit overscroll behavior
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // This sample call will run to the end of the fling animation
+ // and will schedule the overscroll animation.
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut,
+ TimeDuration::FromMilliseconds(10000));
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // At this point, we have an active overscroll animation.
+ // Check that cancelling the animation clears the overscroll.
+ apzc->CancelAnimation();
+ EXPECT_FALSE(apzc->IsOverscrolled());
+ apzc->AssertStateIsReset();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverScrollPanningAbort) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Pan sufficiently to hit overscroll behaviour. Keep the finger down so
+ // the pan does not end.
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd, PanOptions::KeepFingerDown);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that calling CancelAnimation() while the user is still panning
+ // (and thus no fling or snap-back animation has had a chance to start)
+ // clears the overscroll.
+ apzc->CancelAnimation();
+ EXPECT_FALSE(apzc->IsOverscrolled());
+ apzc->AssertStateIsReset();
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByVerticalPanGestures) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1767337) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Send two PANGESTURE_END in a row, to see if the second one gets us
+ // stuck in overscroll.
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true);
+ SampleAnimationOnce();
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true);
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByVerticalAndHorizontalPanGestures) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByPanMomentumGestures) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we are not yet in overscrolled region.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, IgnoreMomemtumDuringOverscroll) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ float yMost = GetScrollRange().YMost();
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, yMost / 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, yMost), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, yMost / 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // And check the overscrolled transform value before/after calling PanGesture
+ // to make sure the overscroll amount isn't affected by momentum events.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ AsyncTransformComponentMatrix overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_EQ(
+ overscrolledTransform,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ EXPECT_EQ(
+ overscrolledTransform,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ EXPECT_EQ(
+ overscrolledTransform,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time());
+ EXPECT_EQ(
+ overscrolledTransform,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling));
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_EQ(
+ overscrolledTransform,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling));
+
+ // Check that we've recovered from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, VerticalOnlyOverscroll) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Make the content scrollable only vertically.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ apzc->SetFrameMetrics(metrics);
+
+ // Scroll up into overscroll a bit.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+ // Now it's overscrolled.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ AsyncTransformComponentMatrix overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ // The overscroll shouldn't happen horizontally.
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ // Happens only vertically.
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ // Send pan momentum events including horizontal bits.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ // The overscroll shouldn't happen horizontally.
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, VerticalOnlyOverscrollByPanMomentum) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Make the content scrollable only vertically.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ // Scrolls the content down a bit.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 50));
+ apzc->SetFrameMetrics(metrics);
+
+ // Scroll up a bit where overscroll will not happen.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure it's not yet overscrolled.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+
+ // Send pan momentum events including horizontal bits.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time());
+ // Now it's overscrolled.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ AsyncTransformComponentMatrix overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ // But the overscroll shouldn't happen horizontally.
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ // Happens only vertically.
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ overscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_TRUE(overscrolledTransform._41 == 0);
+ EXPECT_TRUE(overscrolledTransform._42 != 0);
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, DisallowOverscrollInSingleLineTextControl) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a horizontal scrollable frame with `vertical disregarded direction`.
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 10));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10));
+ apzc->SetFrameMetrics(metrics);
+ metadata.SetDisregardedDirection(Some(ScrollDirection::eVertical));
+ apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false,
+ /*aThisLayerTreeUpdated=*/true);
+
+ // Try to overscroll up and left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(-10, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 5),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // No overscrolling should happen.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+
+ // Send pan momentum events too.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(-100, -100), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(-50, -50), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time());
+ // No overscrolling should happen either.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+// Tests that horizontal overscroll animation keeps running with vertical
+// pan momentum scrolling.
+TEST_F(APZCOverscrollTester,
+ HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000));
+ apzc->SetFrameMetrics(metrics);
+
+ // Try to overscroll left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ AsyncTransformComponentMatrix initialOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+
+ // Send lengthy downward momentums to make sure the overscroll animation
+ // doesn't clobber the momentums scrolling.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount on X axis has started being managed by the overscroll
+ // animation.
+ AsyncTransformComponentMatrix currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41);
+ // There is no overscroll on Y axis.
+ EXPECT_EQ(currentOverscrolledTransform._42, 0);
+ ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ // The scroll offset shouldn't be changed by the overscroll animation.
+ EXPECT_EQ(scrollOffset.y, 0);
+
+ // Simple gesture on the Y axis to ensure that we can send a vertical
+ // momentum scroll
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ ParentLayerPoint offsetAfterPan = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by this pan
+ // momentum start event since the displacement is zero.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount should be managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ // Not yet started scrolling.
+ EXPECT_EQ(scrollOffset.y, offsetAfterPan.y);
+ EXPECT_EQ(scrollOffset.x, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+
+ // Send a long pan momentum.
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // Now it started scrolling vertically.
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ EXPECT_GT(scrollOffset.y, 0);
+ EXPECT_EQ(scrollOffset.x, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll on X axis keeps being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // The scroll offset on Y axis shouldn't be changed by the overscroll
+ // animation.
+ EXPECT_EQ(scrollOffset.y, apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ scrollOffset.y);
+
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ scrollOffset.y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // This momentum event doesn't change the scroll offset since its
+ // displacement is zero.
+ EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ scrollOffset.y);
+
+ // Check that we recover from the horizontal overscroll via the animation.
+ ParentLayerPoint expectedScrollOffset(0, scrollOffset.y);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+// Similar to above
+// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling,
+// but having OverscrollAnimation on both axes initially.
+TEST_F(APZCOverscrollTester,
+ BothAxesOverscrollAnimationWithPanMomentumScrolling) {
+ // TODO: This test currently requires gestures that cause movement on both
+ // axis, which excludes DOMINANT_AXIS locking mode. The gestures should be
+ // broken up into multiple gestures to cause the overscroll.
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2);
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000));
+ apzc->SetFrameMetrics(metrics);
+
+ // Try to overscroll up and left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ AsyncTransformComponentMatrix initialOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+
+ // Send lengthy downward momentums to make sure the overscroll animation
+ // doesn't clobber the momentums scrolling.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount has started being managed by the overscroll
+ // animation.
+ AsyncTransformComponentMatrix currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41);
+ EXPECT_NE(initialOverscrolledTransform._42, currentOverscrolledTransform._42);
+
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by this pan
+ // momentum start event since the displacement is zero.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // Still being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ EXPECT_NE(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ // Send a long pan momentum.
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // But now the overscroll amount on Y axis should be changed by this momentum
+ // pan.
+ EXPECT_NE(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+ // Actually it's no longer overscrolled.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42,
+ 0);
+
+ ParentLayerPoint currentScrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ // Now it started scrolling.
+ EXPECT_GT(currentScrollOffset.y, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll on X axis keeps being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // But the overscroll on Y axis is no longer affected by the overscroll
+ // animation.
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+ // The scroll offset on Y axis shouldn't be changed by the overscroll
+ // animation.
+ EXPECT_EQ(currentScrollOffset.y,
+ apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ currentScrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // Keeping no overscrolling on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42,
+ 0);
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ currentScrollOffset.y);
+
+ currentScrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // Keeping no overscrolling on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42,
+ 0);
+ // Scrolling keeps going by momentum.
+ EXPECT_GT(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ currentScrollOffset.y);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ currentScrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // Keeping no overscrolling on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42,
+ 0);
+ // This momentum event doesn't change the scroll offset since its
+ // displacement is zero.
+ EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ currentScrollOffset.y);
+
+ // Check that we recover from the horizontal overscroll via the animation.
+ ParentLayerPoint expectedScrollOffset(0, currentScrollOffset.y);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android
+// This is another variant of
+// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling. In this test,
+// after a horizontal overscroll animation started, upwards pan moments happen,
+// thus there should be a new vertical overscroll animation in addition to
+// the horizontal one.
+TEST_F(
+ APZCOverscrollTester,
+ VerticalOverscrollAnimationInAdditionToExistingHorizontalOverscrollAnimation) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000));
+ // Scrolls the content 50px down.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 50));
+ apzc->SetFrameMetrics(metrics);
+
+ // Try to overscroll left with pan gestures.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(-2, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Make sure we've started an overscroll animation.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ AsyncTransformComponentMatrix initialOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+
+ // Send lengthy __upward__ momentums to make sure the overscroll animation
+ // doesn't clobber the momentums scrolling.
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount on X axis has started being managed by the overscroll
+ // animation.
+ AsyncTransformComponentMatrix currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41);
+ // There is no overscroll on Y axis.
+ EXPECT_EQ(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42,
+ 0);
+ ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ // The scroll offset shouldn't be changed by the overscroll animation.
+ EXPECT_EQ(scrollOffset.y, 50);
+
+ // Simple gesture on the Y axis to ensure that we can send a vertical
+ // momentum scroll
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ ParentLayerPoint offsetAfterPan = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by this pan
+ // momentum start event since the displacement is zero.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll amount should be managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ // Not yet started scrolling.
+ EXPECT_EQ(scrollOffset.y, offsetAfterPan.y);
+ EXPECT_EQ(scrollOffset.x, 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+
+ // Send a long pan momentum.
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -200), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // Now it started scrolling vertically.
+ scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ EXPECT_EQ(scrollOffset.y, 0);
+ EXPECT_EQ(scrollOffset.x, 0);
+ // Actually it's also vertically overscrolled.
+ EXPECT_GT(
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42,
+ 0);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // The overscroll on X axis keeps being managed by the overscroll animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // The overscroll on Y Axis hasn't been changed by the overscroll animation at
+ // this moment, sine the last displacement was consumed in the last pan
+ // momentum.
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on X axis shouldn't be changed by this momentum pan.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // Now the overscroll amount on Y axis shouldn't be changed by this momentum
+ // pan either.
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ EXPECT_NE(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ // And now the overscroll on Y Axis should be also managed by the overscroll
+ // animation.
+ EXPECT_NE(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -10), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+ // The overscroll amount on both axes shouldn't be changed by momentum event.
+ EXPECT_EQ(
+ currentOverscrolledTransform._41,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._41);
+ EXPECT_EQ(
+ currentOverscrolledTransform._42,
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)
+ ._42);
+
+ currentOverscrolledTransform =
+ apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // Check that we recover from the horizontal overscroll via the animation.
+ ParentLayerPoint expectedScrollOffset(0, 0);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTester, OverscrollByPanGesturesInterruptedByReflowZoom) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_INT("mousewheel.with_control.action", 3); // reflow zoom.
+
+ // A sanity check that pan gestures with ctrl modifier will not be handled by
+ // APZ.
+ PanGestureInput panInput(PanGestureInput::PANGESTURE_START, mcc->Time(),
+ ScreenIntPoint(5, 5), ScreenPoint(0, -2),
+ MODIFIER_CONTROL);
+ WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr);
+ EXPECT_FALSE(APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome());
+
+ ScrollableLayerGuid rootGuid = CreateSimpleRootScrollableForWebRender();
+ RefPtr<AsyncPanZoomController> apzc =
+ tm->GetTargetAPZC(rootGuid.mLayersId, rootGuid.mScrollId);
+
+ PanGesture(PanGestureInput::PANGESTURE_START, tm, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, tm, ScreenIntPoint(50, 80),
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Make sure overscrolling has started.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Press ctrl until PANGESTURE_END.
+ PanGestureWithModifiers(PanGestureInput::PANGESTURE_PAN, MODIFIER_CONTROL, tm,
+ ScreenIntPoint(50, 80), ScreenPoint(0, -2),
+ mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ // At this moment (i.e. PANGESTURE_PAN), still in overscrolling state.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ PanGestureWithModifiers(PanGestureInput::PANGESTURE_END, MODIFIER_CONTROL, tm,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0),
+ mcc->Time());
+ // The overscrolling state should have been restored.
+ EXPECT_TRUE(!apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, SmoothTransitionFromPanToAnimation) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ // Start scrolled down to y=500px.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 500));
+ apzc->SetFrameMetrics(metrics);
+
+ int frameLength = 10; // milliseconds; 10 to keep the math simple
+ float panVelocity = 10; // pixels per millisecond
+ int panPixelsPerFrame = frameLength * panVelocity; // 100 pixels per frame
+
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -1), mcc->Time());
+ // Pan up for 6 frames at 100 pixels per frame. This should reduce
+ // the vertical scroll offset from 500 to 0, and get us into overscroll.
+ for (int i = 0; i < 6; ++i) {
+ mcc->AdvanceByMillis(frameLength);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -panPixelsPerFrame), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Pan further into overscroll at the same input velocity, enough
+ // for the frames while we are in overscroll to dominate the computation
+ // in the velocity tracker.
+ // Importantly, while the input velocity is still 100 pixels per frame,
+ // in the overscrolled state the page only visual moves by at most 8 pixels
+ // per frame.
+ int frames = StaticPrefs::apz_velocity_relevance_time_ms() / frameLength;
+ for (int i = 0; i < frames; ++i) {
+ mcc->AdvanceByMillis(frameLength);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -panPixelsPerFrame), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // End the pan, allowing an overscroll animation to start.
+ mcc->AdvanceByMillis(frameLength);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // Check that the velocity reflects the actual movement (no more than 8
+ // pixels/frame ==> 0.8 pixels per millisecond), not the input velocity
+ // (100 pixels/frame ==> 10 pixels per millisecond). This ensures that
+ // the transition from the pan to the animation appears smooth.
+ // (Note: velocities are negative since they are upwards.)
+ EXPECT_LT(apzc->GetVelocityVector().y, 0);
+ EXPECT_GT(apzc->GetVelocityVector().y, -0.8);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, NoOverscrollForMousewheel) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ // Start scrolled down just a few pixels from the top.
+ metrics.SetVisualScrollOffset(CSSPoint(0, 3));
+ // Set line and page scroll amounts. Otherwise, even though Wheel() uses
+ // SCROLLDELTA_PIXEL, the wheel handling code will get confused by things
+ // like the "don't scroll more than one page" check.
+ metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100));
+ metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10));
+ apzc->SetScrollMetadata(metadata);
+
+ // Send a wheel with enough delta to scrollto y=0 *and* overscroll.
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time());
+
+ // Check that we did not actually go into overscroll.
+ EXPECT_FALSE(apzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, ClickWhileOverscrolled) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ apzc->SetFrameMetrics(metrics);
+
+ // Pan into overscroll at the top.
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -1), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -100), mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top
+
+ // End the pan. This should start an overscroll animation.
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ // Send a mouse-down. This should interrupt the animation but not relieve
+ // overscroll yet.
+ ParentLayerPoint overscrollBefore = apzc->GetOverscrollAmount();
+ MouseDown(apzc, panPoint, mcc->Time());
+ EXPECT_FALSE(apzc->IsOverscrollAnimationRunning());
+ EXPECT_EQ(overscrollBefore, apzc->GetOverscrollAmount());
+
+ // Send a mouse-up. This should start an overscroll animation again.
+ MouseUp(apzc, panPoint, mcc->Time());
+ EXPECT_TRUE(apzc->IsOverscrollAnimationRunning());
+
+ SampleAnimationUntilRecoveredFromOverscroll(ParentLayerPoint(0, 0));
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, DynamicallyLoadingContent) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+ metrics.SetVisualScrollOffset(CSSPoint(0, 0));
+ apzc->SetFrameMetrics(metrics);
+
+ // Pan to the bottom of the page, and further, into overscroll.
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, 1), mcc->Time());
+ for (int i = 0; i < 12; ++i) {
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, 100), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y > 0); // overscrolled at bottom
+
+ // Grow the scrollable rect at the bottom, simulating the page loading content
+ // dynamically.
+ CSSRect scrollableRect = metrics.GetScrollableRect();
+ scrollableRect.height += 500;
+ metrics.SetScrollableRect(scrollableRect);
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Check that the modified scrollable rect cleared the overscroll.
+ EXPECT_FALSE(apzc->IsOverscrolled());
+
+ // Pan back up to the top, and further, into overscroll.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -1), mcc->Time());
+ for (int i = 0; i < 12; ++i) {
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -100), mcc->Time());
+ }
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ ParentLayerPoint overscrollAmount = apzc->GetOverscrollAmount();
+ EXPECT_TRUE(overscrollAmount.y < 0); // overscrolled at top
+
+ // Grow the scrollable rect at the bottom again.
+ scrollableRect = metrics.GetScrollableRect();
+ scrollableRect.height += 500;
+ metrics.SetScrollableRect(scrollableRect);
+ apzc->NotifyLayersUpdated(metadata, false, true);
+
+ // Check that the modified scrollable rect did NOT clear overscroll at the
+ // top.
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_EQ(overscrollAmount,
+ apzc->GetOverscrollAmount()); // overscroll did not change at all
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTester, SmallAmountOfOverscroll) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+
+ // Do vertical overscroll first.
+ ScreenIntPoint panPoint(50, 50);
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ mcc->AdvanceByMillis(10);
+
+ // Then do small horizontal overscroll which will be considered as "finished"
+ // by our overscroll animation physics model.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(-0.1, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(-0.2, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+ mcc->AdvanceByMillis(10);
+
+ EXPECT_TRUE(apzc->IsOverscrolled());
+ EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top
+ EXPECT_TRUE(apzc->GetOverscrollAmount().x < 0); // and overscrolled at left
+
+ // Then do vertical scroll.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint,
+ ScreenPoint(0, 100), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0),
+ mcc->Time());
+
+ ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling);
+ EXPECT_GT(scrollOffset.y, 0); // Make sure the vertical scroll offset is
+ // greater than zero.
+
+ // The small horizontal overscroll amount should be restored to zero.
+ ParentLayerPoint expectedScrollOffset(0, scrollOffset.y);
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID // Only applies to WidgetOverscrollEffect
+TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1786452) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000));
+
+ // Over the course of the test, expect one or more calls to
+ // UpdateOverscrollOffset(), followed by a call to UpdateOverscrollVelocity().
+ // The latter ensures the widget has a chance to end its overscroll effect.
+ InSequence s;
+ EXPECT_CALL(*mcc, UpdateOverscrollOffset(_, _, _, _)).Times(AtLeast(1));
+ EXPECT_CALL(*mcc, UpdateOverscrollVelocity(_, _, _, _)).Times(1);
+
+ // Pan into overscroll, keeping the finger down
+ ScreenIntPoint startPoint(10, 500);
+ ScreenIntPoint endPoint(10, 10);
+ Pan(apzc, startPoint, endPoint, PanOptions::KeepFingerDown);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Linger a while to cause the velocity to drop to very low or zero
+ mcc->AdvanceByMillis(100);
+ TouchMove(apzc, endPoint, mcc->Time());
+ EXPECT_LT(apzc->GetVelocityVector().Length(),
+ StaticPrefs::apz_fling_min_velocity_threshold());
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Lift the finger
+ mcc->AdvanceByMillis(20);
+ TouchUp(apzc, endPoint, mcc->Time());
+ EXPECT_FALSE(apzc->IsOverscrolled());
+}
+#endif
+
+class APZCOverscrollTesterMock : public APZCTreeManagerTester {
+ public:
+ APZCOverscrollTesterMock() { CreateMockHitTester(); }
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, OverscrollHandoff) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ // same size as the visible region so that
+ // the container is not scrollable in any directions
+ // actually. This is simulating overflow: hidden
+ // iframe document in Fission, though we don't set
+ // a different layers id.
+ CSSRect(0, 0, 100, 50));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A pan gesture on the child scroller (which is not scrollable though).
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, VerticalOverscrollHandoffToScrollableRoot) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having two vertical scrollable layers.
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A vertical pan gesture on the child scroller which will be handed off to
+ // the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffToNonScrollableRoot) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having non-scrollable root and a vertical scrollable
+ // child.
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A vertical pan gesture on the child scroller which should not be handed
+ // off the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffOrthogonalPanGesture) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having horizontal scrollable root and a vertical
+ // scrollable child.
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 100));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ // A vertical pan gesture on the child scroller which should not be handed
+ // off the root APZC because the root APZC is not scrollable vertically.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20),
+ ScreenPoint(0, -2), mcc->Time());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTesterMock,
+ RetriggerCancelledOverscrollAnimationByNewPanGesture) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Create a layer tree having vertical scrollable root and a horizontal
+ // scrollable child.
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 200, 50));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ ScreenIntPoint panPoint(50, 20);
+ // A vertical pan gesture on the child scroller which should be handed off the
+ // root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint,
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // The root APZC should be overscrolled and the child APZC should not be.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+
+ mcc->AdvanceByMillis(10);
+
+ // Make sure the root APZC is still overscrolled.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Start a new horizontal pan gesture on the child scroller which should be
+ // handled by the child APZC now.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ APZEventResult result = PanGesture(PanGestureInput::PANGESTURE_START, manager,
+ panPoint, ScreenPoint(-2, 0), mcc->Time());
+ // The above horizontal pan start event was flagged as "this event may trigger
+ // swipe" and either the root scrollable frame or the horizontal child
+ // scrollable frame is not scrollable in the pan start direction, thus the pan
+ // start event run into the short circuit path for swipe-to-navigation in
+ // InputQueue::ReceivePanGestureInput, which means it's waiting for the
+ // content response, so we need to respond explicitly here.
+ manager->ContentReceivedInputBlock(result.mInputBlockId, false);
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(-10, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Now both APZCs should be overscrolled.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled());
+
+ // Sample all animations until all of them have been finished.
+ while (SampleAnimationsOnce())
+ ;
+
+ // After the animations finished, all overscrolled states should have been
+ // restored.
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTesterMock, RetriggeredOverscrollAnimationVelocity) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ // Setup two nested vertical scrollable frames.
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+
+ SetScrollHandoff(layers[1], root);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true);
+
+ ScreenIntPoint panPoint(50, 20);
+ // A vertical upward pan gesture on the child scroller which should be handed
+ // off the root APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint,
+ ScreenPoint(0, -2), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(0, -10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // The root APZC should be overscrolled and the child APZC should not be.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled());
+
+ mcc->AdvanceByMillis(10);
+
+ // Make sure the root APZC is still overscrolled and there's an overscroll
+ // animation.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning());
+
+ // And make sure the overscroll animation's velocity is a certain amount in
+ // the upward direction.
+ EXPECT_LT(rootApzc->GetVelocityVector().y, 0);
+
+ // Start a new downward pan gesture on the child scroller which
+ // should be handled by the child APZC now.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint,
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ // The new pan-start gesture stops the overscroll animation at this moment.
+ EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ // There's no overscroll animation yet even if the root APZC is still
+ // overscrolled.
+ EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
+ ScreenPoint(0, 10), mcc->Time());
+
+ // Now an overscroll animation should have been triggered by the pan-end
+ // gesture.
+ EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ // And the newly created overscroll animation's positions should never exceed
+ // 0.
+ while (SampleAnimationsOnce()) {
+ EXPECT_LE(rootApzc->GetOverscrollAmount().y, 0);
+ }
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect
+TEST_F(APZCOverscrollTesterMock, OverscrollIntoPreventDefault) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRects[] = {LayerIntRect(0, 0, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRects);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+
+ // Start a pan gesture a few pixels below the 20px DTC region.
+ ScreenIntPoint cursorLocation(10, 25);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ APZEventResult result =
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, cursorLocation,
+ ScreenPoint(0, -2), mcc->Time());
+
+ // At this point, we should be overscrolled.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Pan further, until the DTC region is under the cursor.
+ // Note that, due to ApplyResistance(), we need a large input delta to cause a
+ // visual transform enough to bridge the 5px to the DTC region.
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation,
+ ScreenPoint(0, -100), mcc->Time());
+
+ // At this point, we are still overscrolled. Record the overscroll amount.
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ float overscrollY = rootApzc->GetOverscrollAmount().y;
+
+ // Send a content response with preventDefault = true.
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/true);
+
+ // The content response has the effect of interrupting the input block
+ // but no processing happens yet (as there are no events in the block).
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y);
+
+ // Send one more pan event. This starts a new, *unconfirmed* input block
+ // (via the "transmogrify" codepath).
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ result = PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation,
+ ScreenPoint(0, -10), mcc->Time());
+
+ // No overscroll occurs (the event is waiting in the queue for confirmation).
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+ EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y);
+
+ // preventDefault the new event as well
+ manager->SetAllowedTouchBehavior(result.mInputBlockId,
+ {AllowedTouchBehavior::VERTICAL_PAN});
+ manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
+ manager->ContentReceivedInputBlock(result.mInputBlockId,
+ /*aPreventDefault=*/true);
+
+ // This should trigger clearing the overscrolling and resetting the state.
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ rootApzc->AssertStateIsReset();
+
+ // If there are momentum events after this point, they should not cause
+ // further scrolling or overscorll.
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ cursorLocation, ScreenPoint(0, -100), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ cursorLocation, ScreenPoint(0, -100), mcc->Time());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+ EXPECT_EQ(rootApzc->GetFrameMetrics().GetVisualScrollOffset(),
+ CSSPoint(0, 0));
+}
+#endif
diff --git a/gfx/layers/apz/test/gtest/TestPanning.cpp b/gfx/layers/apz/test/gtest/TestPanning.cpp
new file mode 100644
index 0000000000..886b0fec99
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPanning.cpp
@@ -0,0 +1,251 @@
+/* -*- 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/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "gtest/gtest.h"
+
+class APZCPanningTester : public APZCBasicTester {
+ protected:
+ void DoPanTest(bool aShouldTriggerScroll, bool aShouldBeConsumed,
+ uint32_t aBehavior) {
+ if (aShouldTriggerScroll) {
+ // Three repaint request for each pan.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(6);
+ } else {
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+ }
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+
+ // Pan down
+ PanAndCheckStatus(apzc, touchStart, touchEnd, aShouldBeConsumed,
+ &allowedTouchBehaviors);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ if (aShouldTriggerScroll) {
+ EXPECT_EQ(ParentLayerPoint(0, -(touchEnd - touchStart)), pointOut);
+ EXPECT_NE(AsyncTransform(), viewTransformOut);
+ } else {
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+ }
+
+ // Clear the fling from the previous pan, or stopping it will
+ // consume the next touchstart
+ apzc->CancelAnimation();
+
+ // Pan back
+ PanAndCheckStatus(apzc, touchEnd, touchStart, aShouldBeConsumed,
+ &allowedTouchBehaviors);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+ }
+
+ void DoPanWithPreventDefaultTest() {
+ MakeApzcWaitForMainThread();
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ uint64_t blockId = 0;
+
+ // Pan down
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(
+ mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+ PanAndCheckStatus(apzc, touchStart, touchEnd, true, &allowedTouchBehaviors,
+ &blockId);
+
+ // Send the signal that content has handled and preventDefaulted the touch
+ // events. This flushes the event queue.
+ apzc->ContentReceivedInputBlock(blockId, true);
+
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+// In the each of the following 4 pan tests we are performing two pan gestures:
+// vertical pan from top to bottom and back - from bottom to top. According to
+// the pointer-events/touch-action spec AUTO and PAN_Y touch-action values allow
+// vertical scrolling while NONE and PAN_X forbid it. The first parameter of
+// DoPanTest method specifies this behavior. However, the events will be marked
+// as consumed even if the behavior in PAN_X, because the user could move their
+// finger horizontally too - APZ has no way of knowing beforehand and so must
+// consume the events.
+TEST_F(APZCPanningTester, PanWithTouchActionAuto) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(true, true,
+ mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN |
+ mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionNone) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(false, false, 0);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionPanX) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(false, false,
+ mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionPanY) {
+ // Velocity bias can cause extra repaint requests.
+ SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0);
+ DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithPreventDefault) {
+ DoPanWithPreventDefaultTest();
+}
+
+TEST_F(APZCPanningTester, PanWithHistoricalTouchData) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0);
+
+ // Simulate the same pan gesture, in three different ways.
+ // We start at y=50, with a 50ms resting period at the start of the pan.
+ // Then we accelerate the finger upwards towards y=10, reaching a 10px/10ms
+ // velocity towards the end of the panning motion.
+ //
+ // The first simulation fires touch move events with 10ms gaps.
+ // The second simulation skips two of the touch move events, simulating
+ // "jank". The third simulation also skips those two events, but reports the
+ // missed positions in the following event's historical coordinates.
+ //
+ // Consequently, the first and third simulation should estimate the same
+ // velocities, whereas the second simulation should estimate a different
+ // velocity because it is missing data.
+
+ // First simulation: full data
+
+ APZEventResult result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ mcc->AdvanceByMillis(50);
+ result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ result = TouchMove(apzc, ScreenIntPoint(0, 40), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ result = TouchMove(apzc, ScreenIntPoint(0, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ auto velocityFromFullDataAsSeparateEvents = apzc->GetVelocityVector();
+ apzc->CancelAnimation();
+
+ mcc->AdvanceByMillis(100);
+
+ // Second simulation: partial data
+
+ result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ mcc->AdvanceByMillis(50);
+ result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time());
+ mcc->AdvanceByMillis(30);
+ result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ auto velocityFromPartialData = apzc->GetVelocityVector();
+ apzc->CancelAnimation();
+
+ mcc->AdvanceByMillis(100);
+
+ // Third simulation: full data via historical data
+
+ result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time());
+ if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId);
+ }
+
+ mcc->AdvanceByMillis(50);
+ result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time());
+ mcc->AdvanceByMillis(30);
+
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ auto singleTouchData = CreateSingleTouchData(0, ScreenIntPoint(0, 20));
+ singleTouchData.mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ mcc->Time() - TimeDuration::FromMilliseconds(20),
+ ScreenIntPoint(0, 40),
+ {},
+ {},
+ 0.0f,
+ 0.0f});
+ singleTouchData.mHistoricalData.AppendElement(
+ SingleTouchData::HistoricalTouchData{
+ mcc->Time() - TimeDuration::FromMilliseconds(10),
+ ScreenIntPoint(0, 30),
+ {},
+ {},
+ 0.0f,
+ 0.0f});
+ mti.mTouches.AppendElement(singleTouchData);
+ result = apzc->ReceiveInputEvent(mti);
+
+ result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time());
+ auto velocityFromFullDataViaHistory = apzc->GetVelocityVector();
+ apzc->CancelAnimation();
+
+ EXPECT_EQ(velocityFromFullDataAsSeparateEvents,
+ velocityFromFullDataViaHistory);
+ EXPECT_NE(velocityFromPartialData, velocityFromFullDataViaHistory);
+}
+
+TEST_F(APZCPanningTester, DuplicatePanEndEvents_Bug1833950) {
+ // Send a pan gesture that triggers a fling animation at the end.
+ // Note that we need at least two _PAN events to have enough samples
+ // in the velocity tracker to compute a fling velocity.
+ PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE,
+ /*aSimulateMomentum=*/true);
+
+ // Give the fling animation a chance to start.
+ SampleAnimationOnce();
+ apzc->AssertStateIsFling();
+
+ // Send a duplicate pan-end event.
+ // This test is just intended to check that doing this doesn't
+ // trigger an assertion failure in debug mode.
+ PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE,
+ /*aSimulateMomentum=*/true);
+}
diff --git a/gfx/layers/apz/test/gtest/TestPinching.cpp b/gfx/layers/apz/test/gtest/TestPinching.cpp
new file mode 100644
index 0000000000..a22d742eb0
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPinching.cpp
@@ -0,0 +1,664 @@
+/* -*- 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/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+class APZCPinchTester : public APZCBasicTester {
+ private:
+ // This (multiplied by apz.touch_start_tolerance) needs to be the hypotenuse
+ // in a Pythagorean triple, along with overcomeTouchToleranceX and
+ // overcomeTouchToleranceY from APZCTesterBase::Pan().
+ // This is because APZCTesterBase::Pan(), when run without the
+ // PanOptions::ExactCoordinates option, will need to first overcome the
+ // touch start tolerance by performing a move of exactly
+ // (apz.touch_start_tolerance * DPI) length.
+ // When moving on both axes at once, we need to use integers for both legs
+ // (overcomeTouchToleranceX and overcomeTouchToleranceY) while making sure
+ // that the hypotenuse is also a round integer number (hence Pythagorean
+ // triples). (The hypotenuse is the length of the movement in this case.)
+ static const int mDPI = 100;
+
+ public:
+ explicit APZCPinchTester(
+ AsyncPanZoomController::GestureBehavior aGestureBehavior =
+ AsyncPanZoomController::DEFAULT_GESTURES)
+ : APZCBasicTester(aGestureBehavior) {}
+
+ void SetUp() override {
+ APZCBasicTester::SetUp();
+ tm->SetDPI(mDPI);
+ }
+
+ protected:
+ FrameMetrics GetPinchableFrameMetrics() {
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 200));
+ fm.SetScrollableRect(CSSRect(0, 0, 980, 1000));
+ fm.SetVisualScrollOffset(CSSPoint(300, 300));
+ fm.SetLayoutViewport(CSSRect(300, 300, 100, 200));
+ fm.SetZoom(CSSToParentLayerScale(2.0));
+ // APZC only allows zooming on the root scrollable frame.
+ fm.SetIsRootContent(true);
+ // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100
+ return fm;
+ }
+
+ void DoPinchTest(bool aShouldTriggerPinch,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr) {
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ if (aShouldTriggerPinch) {
+ // One repaint request for each gesture.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(2);
+ } else {
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+ }
+
+ int touchInputId = 0;
+ if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) {
+ PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25,
+ touchInputId, aShouldTriggerPinch,
+ aAllowedTouchBehaviors);
+ } else {
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25,
+ aShouldTriggerPinch);
+ }
+
+ apzc->AssertStateIsReset();
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+
+ if (aShouldTriggerPinch) {
+ // the visible area of the document in CSS pixels is now x=325 y=330 w=40
+ // h=80
+ EXPECT_EQ(2.5f, fm.GetZoom().scale);
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(330, fm.GetVisualScrollOffset().y);
+ } else {
+ // The frame metrics should stay the same since touch-action:none makes
+ // apzc ignore pinch gestures.
+ EXPECT_EQ(2.0f, fm.GetZoom().scale);
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().y);
+ }
+
+ // part 2 of the test, move to the top-right corner of the page and pinch
+ // and make sure we stay in the correct spot
+ fm.SetZoom(CSSToParentLayerScale(2.0));
+ fm.SetVisualScrollOffset(CSSPoint(930, 5));
+ apzc->SetFrameMetrics(fm);
+ // the visible area of the document in CSS pixels is x=930 y=5 w=50 h=100
+
+ if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) {
+ PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5,
+ touchInputId, aShouldTriggerPinch,
+ aAllowedTouchBehaviors);
+ } else {
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5,
+ aShouldTriggerPinch);
+ }
+
+ apzc->AssertStateIsReset();
+
+ fm = apzc->GetFrameMetrics();
+
+ if (aShouldTriggerPinch) {
+ // the visible area of the document in CSS pixels is now x=805 y=0 w=100
+ // h=200
+ EXPECT_EQ(1.0f, fm.GetZoom().scale);
+ EXPECT_EQ(805, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(0, fm.GetVisualScrollOffset().y);
+ } else {
+ EXPECT_EQ(2.0f, fm.GetZoom().scale);
+ EXPECT_EQ(930, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(5, fm.GetVisualScrollOffset().y);
+ }
+ }
+};
+
+class APZCPinchGestureDetectorTester : public APZCPinchTester {
+ public:
+ APZCPinchGestureDetectorTester()
+ : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {}
+
+ void DoPinchWithPreventDefaultTest() {
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25,
+ PinchOptions().OutInputBlockId(&blockId));
+
+ // Send the prevent-default notification for the touch block
+ apzc->ContentReceivedInputBlock(blockId, true);
+
+ // verify the metrics didn't change (i.e. the pinch was ignored)
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x,
+ fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y,
+ fm.GetVisualScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+class APZCPinchLockingTester : public APZCPinchTester {
+ private:
+ ScreenIntPoint mFocus;
+ float mSpan;
+ int mPinchLockBufferMaxAge;
+
+ public:
+ APZCPinchLockingTester()
+ : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR),
+ mFocus(ScreenIntPoint(200, 300)),
+ mSpan(10.0) {}
+
+ virtual void SetUp() {
+ mPinchLockBufferMaxAge =
+ StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup();
+
+ APZCPinchTester::SetUp();
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+ mFocus, mSpan, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1));
+ }
+
+ void twoFingerPan() {
+ ScreenCoord panDistance =
+ StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * 1.2 *
+ tm->GetDPI();
+
+ mFocus = ScreenIntPoint((int)(mFocus.x.value + panDistance),
+ (int)(mFocus.y.value));
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ mFocus, mSpan, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1));
+ }
+
+ void twoFingerZoom() {
+ float pinchDistance =
+ StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 1.2 *
+ tm->GetDPI();
+
+ float newSpan = mSpan + pinchDistance;
+
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ mFocus, newSpan, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1));
+ mSpan = newSpan;
+ }
+
+ bool isPinchLockActive() {
+ FrameMetrics originalMetrics = apzc->GetFrameMetrics();
+
+ // Send a small scale input to the APZC
+ float pinchDistance =
+ StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 0.8 *
+ tm->GetDPI();
+ auto event =
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus,
+ mSpan + pinchDistance, mSpan, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+
+ FrameMetrics result = apzc->GetFrameMetrics();
+ bool lockActive = originalMetrics.GetZoom() == result.GetZoom() &&
+ originalMetrics.GetVisualScrollOffset().x ==
+ result.GetVisualScrollOffset().x &&
+ originalMetrics.GetVisualScrollOffset().y ==
+ result.GetVisualScrollOffset().y;
+
+ // Avoid side effects, reset to original frame metrics
+ apzc->SetFrameMetrics(originalMetrics);
+ return lockActive;
+ }
+};
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionNone) {
+ nsTArray<uint32_t> behaviors = {mozilla::layers::AllowedTouchBehavior::NONE,
+ mozilla::layers::AllowedTouchBehavior::NONE};
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionZoom) {
+ nsTArray<uint32_t> behaviors;
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ DoPinchTest(true, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionNotAllowZoom) {
+ nsTArray<uint32_t> behaviors;
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::NONE);
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Pinch_UseGestureDetector_TouchActionNone_NoAPZZoom) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ // Since we are preventing the pinch action via touch-action we should not be
+ // sending the pinch gesture notifications that would normally be sent when
+ // apz_allow_zooming is false.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0);
+ nsTArray<uint32_t> behaviors = {mozilla::layers::AllowedTouchBehavior::NONE,
+ mozilla::layers::AllowedTouchBehavior::NONE};
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault) {
+ DoPinchWithPreventDefaultTest();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault_NoAPZZoom) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ // Since we are preventing the pinch action we should not be sending the pinch
+ // gesture notifications that would normally be sent when apz_allow_zooming is
+ // false.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0);
+
+ DoPinchWithPreventDefaultTest();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomDisabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // Perform a two finger pan
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1,
+ PinchOptions().SecondFocus(ScreenIntPoint(100, 100)));
+
+ // Expect to be in a flinging state
+ apzc->AssertStateIsFling();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_DoesntFling_ZoomDisabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // Perform a pinch
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 2,
+ PinchOptions()
+ .Flags(PinchFlags::LiftFinger2)
+ .Vertical(true)
+ .SecondFocus(ScreenIntPoint(100, 100)));
+
+ // Lift second finger after a pause
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(50));
+ TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time());
+
+ // Pinch should not trigger a fling
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomEnabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ // Perform a two finger pan
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1,
+ PinchOptions().SecondFocus(ScreenIntPoint(100, 100)));
+
+ // Expect to NOT be in flinging state
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Panning_TwoThenOneFingerFling_ZoomEnabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ // Perform a two finger pan lifting only the first finger
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1,
+ PinchOptions()
+ .Flags(PinchFlags::LiftFinger2)
+ .SecondFocus(ScreenIntPoint(100, 100)));
+
+ // Lift second finger after a pause
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(50));
+ TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time());
+
+ // This gesture should activate the pinch lock, and result
+ // in a fling even if the page is zoomable.
+ apzc->AssertStateIsFling();
+}
+
+TEST_F(APZCPinchGestureDetectorTester,
+ Panning_TwoThenOneFingerFling_ZoomDisabled) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // Perform a two finger pan lifting only the first finger
+ PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1,
+ PinchOptions()
+ .Flags(PinchFlags::LiftFinger2)
+ .SecondFocus(ScreenIntPoint(100, 100)));
+
+ // Lift second finger after a pause
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(50));
+ TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time());
+
+ // This gesture should activate the pinch lock and result in a fling
+ apzc->AssertStateIsFling();
+}
+
+TEST_F(APZCPinchTester, Panning_TwoFinger_ZoomDisabled) {
+ // set up APZ
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ nsEventStatus statuses[3]; // scalebegin, scale, scaleend
+ PinchWithPinchInput(apzc, ScreenIntPoint(250, 350), ScreenIntPoint(200, 300),
+ 10, &statuses);
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+
+ // It starts from (300, 300), then moves the focus point from (250, 350) to
+ // (200, 300) pans by (50, 50) screen pixels, but there is a 2x zoom, which
+ // causes the scroll offset to change by half of that (25, 25) pixels.
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(2.0, fm.GetZoom().scale);
+}
+
+TEST_F(APZCPinchTester, Panning_Beyond_LayoutViewport) {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 0);
+
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ // Case 1 - visual viewport is still inside layout viewport.
+ Pan(apzc, 350, 300, PanOptions::NoFling);
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ // It starts from (300, 300) pans by (0, 50) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that (0, 25).
+ // But the visual viewport is still inside the layout viewport.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(325, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(300, fm.GetLayoutViewport().X());
+ EXPECT_EQ(300, fm.GetLayoutViewport().Y());
+
+ // Case 2 - visual viewport crosses the bottom boundary of the layout
+ // viewport.
+ Pan(apzc, 525, 325, PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 325) pans by (0, 200) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (0, 100). The visual viewport crossed the bottom boundary of the layout
+ // viewport by 25px.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(425, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(300, fm.GetLayoutViewport().X());
+ EXPECT_EQ(325, fm.GetLayoutViewport().Y());
+
+ // Case 3 - visual viewport crosses the top boundary of the layout viewport.
+ Pan(apzc, 425, 775, PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 425) pans by (0, -350) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (0, -175). The visual viewport crossed the top of the layout viewport by
+ // 75px.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(250, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(300, fm.GetLayoutViewport().X());
+ EXPECT_EQ(250, fm.GetLayoutViewport().Y());
+
+ // Case 4 - visual viewport crosses the left boundary of the layout viewport.
+ Pan(apzc, ScreenIntPoint(150, 10), ScreenIntPoint(350, 10),
+ PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 250) pans by (-200, 0) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (-100, 0). The visual viewport crossed the left boundary of the layout
+ // viewport by 100px.
+ EXPECT_EQ(200, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(250, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(200, fm.GetLayoutViewport().X());
+ EXPECT_EQ(250, fm.GetLayoutViewport().Y());
+
+ // Case 5 - visual viewport crosses the right boundary of the layout viewport.
+ Pan(apzc, ScreenIntPoint(350, 10), ScreenIntPoint(150, 10),
+ PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (200, 250) pans by (200, 0) screen pixels, but there is a
+ // 2x zoom, which causes the scroll offset to change by half of that
+ // (100, 0). The visual viewport crossed the right boundary of the layout
+ // viewport by 50px.
+ EXPECT_EQ(300, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(250, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(250, fm.GetLayoutViewport().X());
+ EXPECT_EQ(250, fm.GetLayoutViewport().Y());
+
+ // Case 6 - visual viewport crosses both the vertical and horizontal
+ // boundaries of the layout viewport by moving diagonally towards the
+ // top-right corner.
+ Pan(apzc, ScreenIntPoint(350, 200), ScreenIntPoint(150, 400),
+ PanOptions::NoFling);
+ fm = apzc->GetFrameMetrics();
+ // It starts from (300, 250) pans by (200, -200) screen pixels, but there is
+ // a 2x zoom, which causes the scroll offset to change by half of that
+ // (100, -100). The visual viewport moved by (100, -100) outside the
+ // boundary of the layout viewport.
+ EXPECT_EQ(400, fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(150, fm.GetVisualScrollOffset().y);
+ EXPECT_EQ(350, fm.GetLayoutViewport().X());
+ EXPECT_EQ(150, fm.GetLayoutViewport().Y());
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_APZZoom_Disabled) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // When apz_allow_zooming is false, the ZoomConstraintsClient produces
+ // ZoomConstraints with mAllowZoom set to false.
+ MakeApzcUnzoomable();
+
+ // With apz_allow_zooming false, we expect the NotifyPinchGesture function to
+ // get called as the pinch progresses, but the metrics shouldn't change.
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE,
+ apzc->GetGuid(), _, _, _))
+ .Times(AtLeast(1));
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+
+ PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25);
+
+ // verify the metrics didn't change (i.e. the pinch was ignored inside APZ)
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x,
+ fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y,
+ fm.GetVisualScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_NoSpan) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // When apz_allow_zooming is false, the ZoomConstraintsClient produces
+ // ZoomConstraints with mAllowZoom set to false.
+ MakeApzcUnzoomable();
+
+ // With apz_allow_zooming false, we expect the NotifyPinchGesture function to
+ // get called as the pinch progresses, but the metrics shouldn't change.
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE,
+ apzc->GetGuid(), _, _, _))
+ .Times(AtLeast(1));
+ EXPECT_CALL(*mcc,
+ NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END,
+ apzc->GetGuid(), _, LayoutDeviceCoord(0), _))
+ .Times(1);
+
+ int inputId = 0;
+ ScreenIntPoint focus(250, 300);
+
+ // Do a pinch holding a zero span and moving the focus by y=100
+
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+ TimeDuration::FromMilliseconds(50);
+ const auto touchBehaviors = Some(nsTArray<uint32_t>{kDefaultTouchBehavior});
+
+ MultiTouchInput mtiStart =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiStart, touchBehaviors);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ focus.y -= 35 + 1; // this is to get over the PINCH_START_THRESHOLD in
+ // GestureEventListener.cpp
+ MultiTouchInput mtiMove1 =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiMove1);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ focus.y -= 100; // do a two-finger scroll of 100 screen pixels
+ MultiTouchInput mtiMove2 =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiMove2);
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ MultiTouchInput mtiEnd =
+ MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiEnd);
+
+ // Done, check the metrics to make sure we scrolled by 100 screen pixels,
+ // which is 50 CSS pixels for the pinchable frame metrics.
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x,
+ fm.GetVisualScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y + 50,
+ fm.GetVisualScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchTester, Pinch_TwoFinger_APZZoom_Disabled_Bug1354185) {
+ // Set up APZ such that mZoomConstraints.mAllowZoom is false.
+ SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false);
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ // We expect a repaint request for scrolling.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+
+ // Send only the PINCHGESTURE_START and PINCHGESTURE_SCALE events,
+ // in order to trigger a call to AsyncPanZoomController::OnScale
+ // but not to AsyncPanZoomController::OnScaleEnd.
+ ScreenIntPoint aFocus(250, 350);
+ ScreenIntPoint aSecondFocus(200, 300);
+ float aScale = 10;
+ auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+ aFocus, 10.0, 10.0, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+
+ event =
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ aSecondFocus, 10.0f * aScale, 10.0, mcc->Time());
+ apzc->ReceiveInputEvent(event);
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Free) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 0); // PINCH_FREE
+
+ twoFingerPan();
+ EXPECT_FALSE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL
+
+ twoFingerPan();
+ EXPECT_TRUE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock_Break) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL
+
+ twoFingerPan();
+ twoFingerZoom();
+ EXPECT_TRUE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY
+
+ twoFingerPan();
+ EXPECT_TRUE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY
+
+ twoFingerPan();
+ twoFingerZoom();
+ EXPECT_FALSE(isPinchLockActive());
+}
+
+TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break_Lock) {
+ SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY
+
+ twoFingerPan();
+ twoFingerZoom();
+ twoFingerPan();
+ EXPECT_TRUE(isPinchLockActive());
+}
diff --git a/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp
new file mode 100644
index 0000000000..e96a5df6e4
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp
@@ -0,0 +1,500 @@
+/* -*- 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/. */
+
+#include "gtest/gtest.h"
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "apz/src/AsyncPanZoomController.h"
+#include "apz/src/InputBlockState.h"
+#include "apz/src/OverscrollHandoffState.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+
+class APZCArePointerEventsConsumable : public APZCTreeManagerTester {
+ public:
+ APZCArePointerEventsConsumable() { CreateMockHitTester(); }
+
+ void CreateSingleElementTree() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true);
+ }
+
+ void CreateScrollHandoffTree() {
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 200, 200),
+ LayerIntRect(50, 50, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 300, 300));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 200, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true);
+ }
+
+ RefPtr<TouchBlockState> CreateTouchBlockStateForApzc(
+ const RefPtr<TestAsyncPanZoomController>& aApzc) {
+ TouchCounter counter{};
+ TargetConfirmationFlags flags{true};
+
+ return new TouchBlockState(aApzc, flags, counter);
+ }
+
+ void UpdateOverscrollBehavior(ScrollableLayerGuid::ViewID aScrollId,
+ OverscrollBehavior aX, OverscrollBehavior aY) {
+ auto* layer = layers[aScrollId - ScrollableLayerGuid::START_SCROLL_ID];
+ ModifyFrameMetrics(layer, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) {
+ OverscrollBehaviorInfo overscroll;
+ overscroll.mBehaviorX = aX;
+ overscroll.mBehaviorY = aY;
+ sm.SetOverscrollBehavior(overscroll);
+ });
+ UpdateHitTestingTree();
+ }
+
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+};
+
+TEST_F(APZCArePointerEventsConsumable, EmptyInput) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ MultiTouchInput touchInput =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+
+ const PointerEventsConsumableFlags expected{false, false};
+ const PointerEventsConsumableFlags actual =
+ apzc->ArePointerEventsConsumable(blockState, touchInput);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, ScrollHorizontally) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with horizontal 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(30, 10), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ // Scroll area 500x500, room to pan x, room to pan y
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x100, no room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 500x100, room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x500, no room to pan x, room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, ScrollVertically) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 30), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ // Scroll area 500x500, room to pan x, room to pan y
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x100, no room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 500x100, room to pan x, no room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100});
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Scroll area 100x500, no room to pan x, room to pan y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, NestedElementCanScroll) {
+ CreateScrollHandoffTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(layers[1]);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ const PointerEventsConsumableFlags expected{true, true};
+ const PointerEventsConsumableFlags actual =
+ apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, NestedElementCannotScroll) {
+ CreateScrollHandoffTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(layers[1]);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ // Set the nested element to have no room to scroll.
+ // Because of the overscroll handoff, we still have room to scroll
+ // in the parent element.
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Set overscroll handoff for the nested element to none.
+ // Because no handoff will happen, we are not able to use the parent's
+ // room to scroll.
+ // Bug 1814886: Once fixed, change expected value to {false, true}.
+ UpdateOverscrollBehavior(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ OverscrollBehavior::None, OverscrollBehavior::None);
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, NotScrollableButZoomable) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ // Make the root have no room to scroll
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+
+ // Make zoomable
+ apzc->UpdateZoomConstraints(ZoomConstraints(
+ true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ // Add a second touch point and therefore make the APZC consider
+ // zoom use cases as well.
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 90), ScreenSize(0, 0), 0, 0));
+
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsProhibitAll) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ // Convert touch input to two-finger pinch
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0));
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0));
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowVerticalScrolling) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::VERTICAL_PAN});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowHorizontalScrolling) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create touch with horizontal 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(80, 60), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors(
+ {AllowedTouchBehavior::HORIZONTAL_PAN});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowPinchZoom) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Create two-finger pinch
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0));
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(50, 50), ScreenSize(0, 0), 0, 0));
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0));
+
+ PointerEventsConsumableFlags expected{};
+ PointerEventsConsumableFlags actual{};
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE});
+ expected = {true, false};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+
+ {
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+ blockState->UpdateSlopState(touchStart, false);
+
+ blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::PINCH_ZOOM});
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+ }
+}
+
+TEST_F(APZCArePointerEventsConsumable, DynamicToolbar) {
+ CreateSingleElementTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc);
+
+ // Create touch with vertical 20 unit scroll
+ MultiTouchInput touchStart =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ touchStart.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 30), ScreenSize(0, 0), 0, 0));
+
+ MultiTouchInput touchMove =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ touchMove.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(60, 40), ScreenSize(0, 0), 0, 0));
+
+ blockState->UpdateSlopState(touchStart, false);
+
+ // Restrict size of scrollable area: No room to pan X, no room to pan Y
+ apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100});
+
+ PointerEventsConsumableFlags actual{};
+ PointerEventsConsumableFlags expected{};
+
+ expected = {false, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+
+ apzc->GetFrameMetrics().SetCompositionSizeWithoutDynamicToolbar(
+ ParentLayerSize{100, 90});
+ UpdateHitTestingTree();
+
+ expected = {true, true};
+ actual = apzc->ArePointerEventsConsumable(blockState, touchMove);
+ EXPECT_EQ(expected, actual);
+}
diff --git a/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp
new file mode 100644
index 0000000000..ae1bb65960
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp
@@ -0,0 +1,809 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZScrollHandoffTester : public APZCTreeManagerTester {
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+
+ void CreateScrollHandoffLayerTree1() {
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 50, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(
+ true); // make root APZC zoomable
+ }
+
+ void CreateScrollHandoffLayerTree2() {
+ const char* treeShape = "x(x(x))";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 50, 100, 50)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 2,
+ CSSRect(-100, -100, 200, 200));
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[1], root);
+ SetScrollHandoff(layers[2], layers[1]);
+ // No ScopedLayerTreeRegistration as that just needs to be done once per
+ // test and this is the second layer tree for a particular test.
+ MOZ_ASSERT(registration);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateScrollHandoffLayerTree3() {
+ const char* treeShape = "x(x(x)x(x))";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100), // root
+ LayerIntRect(0, 0, 100, 50), // scrolling parent 1
+ LayerIntRect(0, 0, 100, 50), // scrolling child 1
+ LayerIntRect(0, 50, 100, 50), // scrolling parent 2
+ LayerIntRect(0, 50, 100, 50) // scrolling child 2
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 2,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[3],
+ ScrollableLayerGuid::START_SCROLL_ID + 3,
+ CSSRect(0, 50, 100, 100));
+ SetScrollableFrameMetrics(layers[4],
+ ScrollableLayerGuid::START_SCROLL_ID + 4,
+ CSSRect(0, 50, 100, 100));
+ SetScrollHandoff(layers[1], layers[0]);
+ SetScrollHandoff(layers[3], layers[0]);
+ SetScrollHandoff(layers[2], layers[1]);
+ SetScrollHandoff(layers[4], layers[3]);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ }
+
+ // Creates a layer tree with a parent layer that is only scrollable
+ // horizontally, and a child layer that is only scrollable vertically.
+ void CreateScrollHandoffLayerTree4() {
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 200, 100));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ // Creates a layer tree with a parent layer that is not scrollable, and a
+ // child layer that is only scrollable vertically.
+ void CreateScrollHandoffLayerTree5() {
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100), // scrolling parent
+ LayerIntRect(0, 50, 100, 50) // scrolling child
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateScrollgrabLayerTree(bool makeParentScrollable = true) {
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100), // scroll-grabbing parent
+ LayerIntRect(0, 20, 100, 80) // child
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ float parentHeight = makeParentScrollable ? 120 : 100;
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, parentHeight));
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 800));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ rootApzc = ApzcOf(root);
+ rootApzc->GetScrollMetadata().SetHasScrollgrab(true);
+ }
+
+ void TestFlingAcceleration() {
+ // Jack up the fling acceleration multiplier so we can easily determine
+ // whether acceleration occured.
+ const float kAcceleration = 100.0f;
+ SCOPED_GFX_PREF_FLOAT("apz.fling_accel_base_mult", kAcceleration);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_fling_velocity", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_pan_velocity", 0.0);
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan once, enough to fully scroll the scrollgrab parent and then scroll
+ // and fling the child.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 70, 40);
+
+ // Give the fling animation a chance to start.
+ SampleAnimationsOnce();
+
+ float childVelocityAfterFling1 = childApzc->GetVelocityVector().y;
+
+ // Pan again.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 70, 40);
+
+ // Give the fling animation a chance to start.
+ // This time it should be accelerated.
+ SampleAnimationsOnce();
+
+ float childVelocityAfterFling2 = childApzc->GetVelocityVector().y;
+
+ // We should have accelerated once.
+ // The division by 2 is to account for friction.
+ EXPECT_GT(childVelocityAfterFling2,
+ childVelocityAfterFling1 * kAcceleration / 2);
+
+ // We should not have accelerated twice.
+ // The division by 4 is to account for friction.
+ EXPECT_LE(childVelocityAfterFling2,
+ childVelocityAfterFling1 * kAcceleration * kAcceleration / 4);
+ }
+
+ void TestCrossApzcAxisLock() {
+ SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+ Pan(childApzc, ScreenIntPoint(10, 60), ScreenIntPoint(15, 90),
+ PanOptions::KeepFingerDown | PanOptions::ExactCoordinates);
+
+ childApzc->AssertAxisLocked(ScrollDirection::eVertical);
+ childApzc->AssertStateIsPanningLockedY();
+ }
+};
+
+class APZScrollHandoffTesterMock : public APZScrollHandoffTester {
+ public:
+ APZScrollHandoffTesterMock() { CreateMockHitTester(); }
+};
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Here we test that if the processing of a touch block is deferred while we
+// wait for content to send a prevent-default message, overscroll is still
+// handed off correctly when the block is processed.
+TEST_F(APZScrollHandoffTester, DeferredInputEventProcessing) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+
+ // Set up the APZC tree.
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Enable touch-listeners so that we can separate the queueing of input
+ // events from them being processed.
+ childApzc->SetWaitForMainThread();
+
+ // Queue input events for a pan.
+ uint64_t blockId = 0;
+ Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId);
+
+ // Allow the pan to be processed.
+ childApzc->ContentReceivedInputBlock(blockId, false);
+ childApzc->ConfirmTarget(blockId);
+
+ // Make sure overscroll was handed off correctly.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Here we test that if the layer structure changes in between two input
+// blocks being queued, and the first block is only processed after the second
+// one has been queued, overscroll handoff for the first block follows
+// the original layer structure while overscroll handoff for the second block
+// follows the new layer structure.
+TEST_F(APZScrollHandoffTester, LayerStructureChangesWhileEventsArePending) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+
+ // Set up an initial APZC tree.
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Enable touch-listeners so that we can separate the queueing of input
+ // events from them being processed.
+ childApzc->SetWaitForMainThread();
+
+ // Queue input events for a pan.
+ uint64_t blockId = 0;
+ Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId);
+
+ // Modify the APZC tree to insert a new APZC 'middle' into the handoff chain
+ // between the child and the root.
+ CreateScrollHandoffLayerTree2();
+ WebRenderLayerScrollData* middle = layers[1];
+ childApzc->SetWaitForMainThread();
+ TestAsyncPanZoomController* middleApzc = ApzcOf(middle);
+
+ // Queue input events for another pan.
+ uint64_t secondBlockId = 0;
+ Pan(childApzc, 30, 90, PanOptions::NoFling, nullptr, nullptr, &secondBlockId);
+
+ // Allow the first pan to be processed.
+ childApzc->ContentReceivedInputBlock(blockId, false);
+ childApzc->ConfirmTarget(blockId);
+
+ // Make sure things have scrolled according to the handoff chain in
+ // place at the time the touch-start of the first pan was queued.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(0, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Allow the second pan to be processed.
+ childApzc->ContentReceivedInputBlock(secondBlockId, false);
+ childApzc->ConfirmTarget(secondBlockId);
+
+ // Make sure things have scrolled according to the handoff chain in
+ // place at the time the touch-start of the second pan was queued.
+ EXPECT_EQ(0, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(-10, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// Test that putting a second finger down on an APZC while a down-chain APZC
+// is overscrolled doesn't result in being stuck in overscroll.
+TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1073250) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Pan(manager, 10, 40, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Put a second finger down.
+ MultiTouchInput secondFingerDown =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ // Use the same touch identifier for the first touch (0) as Pan(). (A bit
+ // hacky.)
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+// This is almost exactly like StuckInOverscroll_Bug1073250, except the
+// APZC receiving the input events for the first touch block is the child
+// (and thus not the same APZC that overscrolls, which is the parent).
+TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1231228) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 60, 90, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Put a second finger down.
+ MultiTouchInput secondFingerDown =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ // Use the same touch identifier for the first touch (0) as Pan(). (A bit
+ // hacky.)
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202a) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ Pan(manager, 60, 90, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Lift the finger, triggering an overscroll animation
+ // (but don't allow it to run).
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put the finger down again, interrupting the animation
+ // and entering the TOUCHING state.
+ TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Lift the finger once again.
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1240202b) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, 60, 90, PanOptions::KeepFingerDown);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Lift the finger, triggering an overscroll animation
+ // (but don't allow it to run).
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put the finger down again, interrupting the animation
+ // and entering the TOUCHING state.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put a second finger down. Since we're in the TOUCHING state,
+ // the "are we panned into overscroll" check will fail and we
+ // will not ignore the second finger, instead entering the
+ // PINCHING state.
+ MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0,
+ TimeStamp(), 0);
+ // Use the same touch identifier for the first touch (0) as TouchDown(). (A
+ // bit hacky.)
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(10, 90), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(10, 80), ScreenSize(0, 0), 0, 0));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ manager->ReceiveInputEvent(secondFingerDown);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTester, OpposingConstrainedAxes_Bug1201098) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree4();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan, causing the child APZC to overscroll.
+ Pan(childApzc, 50, 60);
+
+ // Make sure only the child is overscrolled.
+ EXPECT_TRUE(childApzc->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+#endif
+
+// Test that flinging in a direction where one component of the fling goes into
+// overscroll but the other doesn't, results in just the one component being
+// handed off to the parent, while the original APZC continues flinging in the
+// other direction.
+TEST_F(APZScrollHandoffTesterMock, PartialFlingHandoff) {
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ // Fling up and to the left. The child APZC has room to scroll up, but not
+ // to the left, so the horizontal component of the fling should be handed
+ // off to the parent APZC.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55));
+
+ RefPtr<TestAsyncPanZoomController> parent = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> child = ApzcOf(layers[1]);
+
+ // Advance the child's fling animation once to give the partial handoff
+ // a chance to occur.
+ mcc->AdvanceByMillis(10);
+ child->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Assert that partial handoff has occurred.
+ child->AssertStateIsFling();
+ parent->AssertStateIsFling();
+}
+
+// Here we test that if two flings are happening simultaneously, overscroll
+// is handed off correctly for each.
+TEST_F(APZScrollHandoffTester, SimultaneousFlings) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ // Set up an initial APZC tree.
+ CreateScrollHandoffLayerTree3();
+
+ RefPtr<TestAsyncPanZoomController> parent1 = ApzcOf(layers[1]);
+ RefPtr<TestAsyncPanZoomController> child1 = ApzcOf(layers[2]);
+ RefPtr<TestAsyncPanZoomController> parent2 = ApzcOf(layers[3]);
+ RefPtr<TestAsyncPanZoomController> child2 = ApzcOf(layers[4]);
+
+ // Pan on the lower child.
+ Pan(child2, 45, 5);
+
+ // Pan on the upper child.
+ Pan(child1, 95, 55);
+
+ // Check that child1 and child2 are in a FLING state.
+ child1->AssertStateIsFling();
+ child2->AssertStateIsFling();
+
+ // Advance the animations on child1 and child2 until their end.
+ child1->AdvanceAnimationsUntilEnd();
+ child2->AdvanceAnimationsUntilEnd();
+
+ // Check that the flings have been handed off to the parents.
+ child1->AssertStateIsReset();
+ parent1->AssertStateIsFling();
+ child2->AssertStateIsReset();
+ parent2->AssertStateIsFling();
+}
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZScrollHandoffTester, Scrollgrab) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+
+ // Set up the layer tree
+ CreateScrollgrabLayerTree();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to fully scroll the scrollgrab parent (20 px)
+ // and leave some more (another 15 px) for the child.
+ Pan(childApzc, 80, 45);
+
+ // Check that the parent and child have scrolled as much as we expect.
+ EXPECT_EQ(20, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(15, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+#endif
+
+TEST_F(APZScrollHandoffTester, ScrollgrabFling) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ // Set up the layer tree
+ CreateScrollgrabLayerTree();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, not enough to fully scroll the scrollgrab parent.
+ Pan(childApzc, 80, 70);
+
+ // Check that it is the scrollgrab parent that's in a fling, not the child.
+ rootApzc->AssertStateIsFling();
+ childApzc->AssertStateIsReset();
+}
+
+TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration1) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ CreateScrollgrabLayerTree(true /* make parent scrollable */);
+
+ // Note: Usually, fling acceleration does not work across handoff, because our
+ // fling acceleration code does not propagate the "fling cancel velocity"
+ // across handoff. However, this test sets apz.fling_min_velocity_threshold to
+ // zero, so the "fling cancel velocity" is allowed to be zero, and fling
+ // acceleration succeeds, almost by accident.
+ TestFlingAcceleration();
+}
+
+TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration2) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+ CreateScrollgrabLayerTree(false /* do not make parent scrollable */);
+ TestFlingAcceleration();
+}
+
+TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to scroll it to its end and have scroll
+ // left to hand off. Since immediate handoff is disallowed, we expect
+ // the leftover scroll not to be handed off.
+ Pan(childApzc, 60, 5);
+
+ // Verify that the parent has not scrolled.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+ EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Pan again on the child. This time, since the child was scrolled to
+ // its end when the gesture began, we expect the scroll to be handed off.
+ Pan(childApzc, 60, 50);
+
+ // Verify that the parent scrolled.
+ EXPECT_EQ(10, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+}
+
+TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Fling) {
+ SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to get very close to the end, so that the
+ // subsequent fling reaches the end and has leftover velocity to hand off.
+ Pan(childApzc, 60, 2);
+
+ // Allow the fling to run its course.
+ childApzc->AdvanceAnimationsUntilEnd();
+ parentApzc->AdvanceAnimationsUntilEnd();
+
+ // Verify that the parent has not scrolled.
+ // The first comparison needs to be an ASSERT_NEAR because the fling
+ // computations are such that the final scroll position can be within
+ // COORDINATE_EPSILON of the end rather than right at the end.
+ ASSERT_NEAR(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y,
+ COORDINATE_EPSILON);
+ EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
+
+ // Pan again on the child. This time, since the child was scrolled to
+ // its end when the gesture began, we expect the scroll to be handed off.
+ Pan(childApzc, 60, 40);
+
+ // Allow the fling to run its course. The fling should also be handed off.
+ childApzc->AdvanceAnimationsUntilEnd();
+ parentApzc->AdvanceAnimationsUntilEnd();
+
+ // Verify that the parent scrolled from the fling.
+ EXPECT_GT(parentApzc->GetFrameMetrics().GetVisualScrollOffset().y, 10);
+}
+
+TEST_F(APZScrollHandoffTester, CrossApzcAxisLock_TouchAction) {
+ TestCrossApzcAxisLock();
+}
+
+TEST_F(APZScrollHandoffTesterMock, WheelHandoffAfterDirectionReversal) {
+ // Explicitly set the wheel transaction timeout pref because the test relies
+ // on its value.
+ SCOPED_GFX_PREF_INT("mousewheel.transaction.timeout", 1500);
+
+ // Set up a basic scroll handoff layer tree.
+ CreateScrollHandoffLayerTree1();
+
+ rootApzc = ApzcOf(layers[0]);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+ FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics();
+ FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
+ CSSRect childScrollRange = childMetrics.CalculateScrollRange();
+
+ EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ ScreenIntPoint cursorLocation(10, 60); // positioned to hit the subframe
+ ScreenPoint upwardDelta(0, -10);
+ ScreenPoint downwardDelta(0, 10);
+
+ // First wheel upwards. This will have no effect because we're already
+ // scrolled to the top.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, cursorLocation, upwardDelta, mcc->Time());
+ EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ // Now wheel downwards 6 times. This should scroll the child, and get it
+ // to the bottom of its 50px scroll range.
+ for (size_t i = 0; i < 6; ++i) {
+ mcc->AdvanceByMillis(100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, cursorLocation, downwardDelta, mcc->Time());
+ }
+ EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
+ EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y);
+
+ // Wheel downwards an additional 16 times, with 100ms increments.
+ // This should be enough to overcome the 1500ms wheel transaction timeout
+ // and start scrolling the root.
+ for (size_t i = 0; i < 16; ++i) {
+ mcc->AdvanceByMillis(100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, cursorLocation, downwardDelta, mcc->Time());
+ }
+ EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y);
+ EXPECT_GT(rootMetrics.GetVisualScrollOffset().y, 0);
+}
+
+TEST_F(APZScrollHandoffTesterMock, WheelHandoffNonscrollable) {
+ // Set up a basic scroll layer tree.
+ CreateScrollHandoffLayerTree5();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+ FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
+
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ ScreenPoint downwardDelta(0, 10);
+ // Positioned to hit the nonscrollable parent frame
+ ScreenIntPoint nonscrollableLocation(40, 10);
+ // Positioned to hit the scrollable subframe
+ ScreenIntPoint scrollableLocation(40, 60);
+
+ // Start the wheel transaction on a nonscrollable parent frame.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ Wheel(manager, nonscrollableLocation, downwardDelta, mcc->Time());
+ EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
+
+ // Mouse moves to a scrollable subframe. This should end the transaction.
+ mcc->AdvanceByMillis(100);
+ MouseInput mouseInput(MouseInput::MOUSE_MOVE,
+ MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0,
+ scrollableLocation, mcc->Time(), 0);
+ WidgetMouseEvent mouseEvent = mouseInput.ToWidgetEvent(nullptr);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ ((APZInputBridge*)manager.get())->ReceiveInputEvent(mouseEvent);
+
+ // Wheel downward should scroll the subframe.
+ mcc->AdvanceByMillis(100);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ Wheel(manager, scrollableLocation, downwardDelta, mcc->Time());
+ EXPECT_GT(childMetrics.GetVisualScrollOffset().y, 0);
+}
+
+TEST_F(APZScrollHandoffTesterMock, ChildCloseToEndOfScrollRange) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics();
+ FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
+
+ // Zoom the page in by 3x. This needs to be reflected in the zoom level
+ // and composition bounds of both APZCs.
+ rootMetrics.SetZoom(CSSToParentLayerScale(3.0));
+ rootMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300));
+ childMetrics.SetZoom(CSSToParentLayerScale(3.0));
+ childMetrics.SetCompositionBounds(ParentLayerRect(0, 150, 300, 150));
+
+ // Scroll the child APZC very close to the end of the scroll range.
+ // The scroll offset is chosen such that in CSS pixels it has 0.01 pixels
+ // room to scroll (less than COORDINATE_EPSILON = 0.02), but in ParentLayer
+ // pixels it has 0.03 pixels room (greater than COORDINATE_EPSILON).
+ childMetrics.SetVisualScrollOffset(CSSPoint(0, 49.99));
+
+ EXPECT_FALSE(childApzc->IsOverscrolled());
+
+ CSSPoint childBefore = childApzc->GetFrameMetrics().GetVisualScrollOffset();
+ CSSPoint parentBefore = rootApzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ // Synthesize a pan gesture that tries to scroll the child further down.
+ PanGesture(PanGestureInput::PANGESTURE_START, childApzc,
+ ScreenIntPoint(10, 20), ScreenPoint(0, 40), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ childApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ PanGesture(PanGestureInput::PANGESTURE_END, childApzc, ScreenIntPoint(10, 21),
+ ScreenPoint(0, 0), mcc->Time());
+
+ CSSPoint childAfter = childApzc->GetFrameMetrics().GetVisualScrollOffset();
+ CSSPoint parentAfter = rootApzc->GetFrameMetrics().GetVisualScrollOffset();
+
+ bool childScrolled = (childBefore != childAfter);
+ bool parentScrolled = (parentBefore != parentAfter);
+
+ // Check that either the child or the parent scrolled.
+ // (With the current implementation of comparing quantities to
+ // COORDINATE_EPSILON in CSS units, it will be the parent, but the important
+ // thing is that at least one of the child or parent scroll, i.e. we're not
+ // stuck in a situation where no scroll offset is changing).
+ EXPECT_TRUE(childScrolled || parentScrolled);
+}
diff --git a/gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp b/gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp
new file mode 100644
index 0000000000..014eb0a4ba
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp
@@ -0,0 +1,102 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZScrollbarDraggingTester : public APZCTreeManagerTester {
+ public:
+ APZScrollbarDraggingTester() { CreateMockHitTester(); }
+
+ protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ ScrollableLayerGuid::ViewID scrollId = ScrollableLayerGuid::START_SCROLL_ID;
+ TestAsyncPanZoomController* apzc = nullptr;
+
+ ParentLayerCoord ScrollY() const {
+ return apzc
+ ->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForEventHandling)
+ .y;
+ }
+
+ void QueueHitOnVerticalScrollbar() {
+ mMockHitTester->QueueScrollbarThumbHitResult(scrollId,
+ ScrollDirection::eVertical);
+ }
+
+ void CreateLayerTreeWithVerticalScrollbar() {
+ // The first child is the scrollable node, the second child is the
+ // scrollbar.
+ const char* treeShape = "x(xx)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 50, 100),
+ LayerIntRect(50, 0, 50, 10)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[1], scrollId, CSSRect(0, 0, 50, 1000));
+ registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ layers[2]->SetScrollbarData(ScrollbarData::CreateForThumb(
+ ScrollDirection::eVertical, 0.1, 0, 10, 10, true, 0, 100, scrollId));
+ UpdateHitTestingTree();
+ apzc = ApzcOf(layers[1]);
+ }
+};
+
+// Test that the scrollable rect shrinking during dragging does not result
+// in scrolling out of bounds.
+TEST_F(APZScrollbarDraggingTester, ScrollableRectShrinksDuringDragging) {
+ // Explicitly enable scrollbar dragging. This allows the test to run on
+ // Android as well.
+ SCOPED_GFX_PREF_BOOL("apz.drag.enabled", true);
+
+ CreateLayerTreeWithVerticalScrollbar();
+ EXPECT_EQ(ScrollY(), 0);
+
+ // Start a scrollbar drag at y=5.
+ QueueHitOnVerticalScrollbar();
+ auto dragBlockId =
+ MouseDown(manager, ScreenIntPoint(75, 5), mcc->Time()).mInputBlockId;
+ manager->StartScrollbarDrag(apzc->GetGuid(),
+ AsyncDragMetrics(scrollId, 0, dragBlockId, 5,
+ ScrollDirection::eVertical));
+
+ // Drag the scrollbar down to y=75. (The total height is 100.)
+ for (int mouseY = 10; mouseY <= 75; mouseY += 5) {
+ mcc->AdvanceByMillis(10);
+ // We do a hit test for every mouse event, including mousemoves.
+ QueueHitOnVerticalScrollbar();
+ MouseMove(manager, ScreenIntPoint(75, mouseY), mcc->Time());
+ }
+
+ // We should have scrolled past y>500 at least (total scrollable rect height
+ // is 1000).
+ EXPECT_GT(ScrollY(), 500);
+
+ // Shrink the scrollable rect height to 500.
+ ModifyFrameMetrics(layers[1], [](ScrollMetadata&, FrameMetrics& aMetrics) {
+ aMetrics.SetScrollableRect(CSSRect(0, 0, 50, 500));
+ });
+ UpdateHitTestingTree();
+
+ // Continue the drag to near the bottom, y=95.
+ // Check that the scroll position never gets out of bounds. (With the
+ // scrollable rect height now 500, the max vertical scroll position is 400.)
+ for (int mouseY = 80; mouseY <= 95; mouseY += 5) {
+ mcc->AdvanceByMillis(10);
+ QueueHitOnVerticalScrollbar();
+ MouseMove(manager, ScreenIntPoint(75, mouseY), mcc->Time());
+ EXPECT_LE(ScrollY(), 400);
+ }
+
+ // End the drag.
+ mcc->AdvanceByMillis(10);
+ QueueHitOnVerticalScrollbar();
+ MouseUp(manager, ScreenIntPoint(75, 95), mcc->Time());
+
+ // We should end up at the bottom of the new scroll range (and not out of
+ // bounds).
+ EXPECT_EQ(ScrollY(), 400);
+}
diff --git a/gfx/layers/apz/test/gtest/TestSnapping.cpp b/gfx/layers/apz/test/gtest/TestSnapping.cpp
new file mode 100644
index 0000000000..60dcf0bee6
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestSnapping.cpp
@@ -0,0 +1,302 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+
+class APZCSnappingTesterMock : public APZCTreeManagerTester {
+ public:
+ APZCSnappingTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZCSnappingTesterMock, Bug1265510) {
+ // Needed because the test uses SmoothWheel()
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+
+ const char* treeShape = "x(x)";
+ LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 100, 100, 100)};
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1,
+ CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize =
+ CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0));
+
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(0 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ TestAsyncPanZoomController* outer = ApzcOf(layers[0]);
+ TestAsyncPanZoomController* inner = ApzcOf(layers[1]);
+
+ // Position the mouse near the bottom of the outer frame and scroll by 60px.
+ // (6 lines of 10px each). APZC will actually scroll to y=100 because of the
+ // mandatory snap coordinate there.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), mcc->Time());
+ // Advance in 5ms increments until we've scrolled by 70px. At this point, the
+ // closest snap point is y=100, and the inner frame should be under the mouse
+ // cursor.
+ while (outer
+ ->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling)
+ .y < 70) {
+ mcc->AdvanceByMillis(5);
+ outer->AdvanceAnimations(mcc->GetSampleTime());
+ }
+ // Now do another wheel in a new transaction. This should start scrolling the
+ // inner frame; we verify that it does by checking the inner scroll position.
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(
+ StaticPrefs::mousewheel_transaction_timeout() + 100));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ inner->AdvanceAnimationsUntilEnd();
+ EXPECT_LT(0.0f, inner
+ ->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling)
+ .y);
+
+ // However, the outer frame should also continue to the snap point, otherwise
+ // it is demonstrating incorrect behaviour by violating the mandatory
+ // snapping.
+ outer->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(100.0f, outer
+ ->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling)
+ .y);
+}
+
+TEST_F(APZCSnappingTesterMock, Snap_After_Pinch) {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 200));
+
+ // Set up some basic scroll snapping
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize =
+ CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0));
+
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(0 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ // Save the scroll snap info on the root APZC.
+ // Also mark the root APZC as "root content", since APZC only allows
+ // zooming on the root content APZC.
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ aMetrics.SetIsRootContent(true);
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Allow zooming
+ apzc->UpdateZoomConstraints(ZoomConstraints(
+ true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+
+ PinchWithPinchInput(apzc, ScreenIntPoint(50, 50), ScreenIntPoint(50, 50),
+ 1.2f);
+
+ apzc->AssertStateIsSmoothMsdScroll();
+}
+
+// Currently fails on Android because on the platform we have a different
+// VelocityTracker.
+#ifndef MOZ_WIDGET_ANDROID
+TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithZeroVelocity) {
+ // Use pref values for desktop everywhere.
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0);
+ SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100);
+
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 400));
+
+ // Set up two snap points, 30 and 100.
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize =
+ CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(30 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ // Save the scroll snap info on the root APZC.
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Send a series of pan gestures to scroll to position at 50.
+ const ScreenIntPoint position = ScreenIntPoint(50, 30);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, position,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position,
+ ScreenPoint(0, 40), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Make sure the velocity just before sending a pan-end is zero.
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, position,
+ ScreenPoint(0, 0), mcc->Time());
+
+ // Now a smooth animation has been triggered for snapping to 30.
+ apzc->AssertStateIsSmoothMsdScroll();
+
+ apzc->AdvanceAnimationsUntilEnd();
+ // The snapped position should be 30 rather than 100 because it's the nearest
+ // snap point.
+ EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ 30);
+}
+
+// Smililar to above SnapOnPanEndWithZeroVelocity but with positive velocity so
+// that the snap position would be the one in the scrolling direction.
+TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithPositiveVelocity) {
+ // Use pref values for desktop everywhere.
+ SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0);
+ SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0);
+ SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100);
+
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 400));
+
+ // Set up two snap points, 30 and 100.
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize =
+ CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(30 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ // Save the scroll snap info on the root APZC.
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ // Send a series of pan gestures that a pan-end event happens at 65
+ const ScreenIntPoint position = ScreenIntPoint(50, 30);
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, position,
+ ScreenPoint(0, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position,
+ ScreenPoint(0, 35), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position,
+ ScreenPoint(0, 20), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // There should be positive velocity in this case.
+ EXPECT_GT(apzc->GetVelocityVector().y, 0);
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, position,
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+
+ // A smooth animation has been triggered by the pan-end event above.
+ apzc->AssertStateIsSmoothMsdScroll();
+
+ apzc->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForEventHandling)
+ .y,
+ 100);
+}
+#endif
diff --git a/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp
new file mode 100644
index 0000000000..a76d1b2d60
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp
@@ -0,0 +1,102 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+
+#include "InputUtils.h"
+#include "mozilla/StaticPrefs_layout.h"
+
+class APZCSnappingOnMomentumTesterMock : public APZCTreeManagerTester {
+ public:
+ APZCSnappingOnMomentumTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZCSnappingOnMomentumTesterMock, Snap_On_Momentum) {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 500));
+
+ // Set up some basic scroll snapping
+ ScrollSnapInfo snap;
+ snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory;
+ snap.mSnapportSize =
+ CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(0 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{1}));
+ snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget(
+ Nothing(), Some(100 * AppUnitsPerCSSPixel()),
+ CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal,
+ ScrollSnapTargetId{2}));
+
+ ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) {
+ aSm.SetSnapInfo(ScrollSnapInfo(snap));
+ });
+
+ UniquePtr<ScopedLayerTreeRegistration> registration =
+ MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ TimeStamp now = mcc->Time();
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 2), now);
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 25), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 25), mcc->Time());
+
+ // The velocity should be positive when panning with positive displacement.
+ EXPECT_GT(apzc->GetVelocityVector().y, 3.0);
+
+ mcc->AdvanceByMillis(5);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 80),
+ ScreenPoint(0, 0), mcc->Time());
+
+ // After lifting the fingers, the velocity should be zero and a smooth
+ // animation should have been triggered for scroll snap.
+ EXPECT_EQ(apzc->GetVelocityVector().y, 0);
+ apzc->AssertStateIsSmoothMsdScroll();
+
+ mcc->AdvanceByMillis(5);
+
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 50), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ apzc->AdvanceAnimations(mcc->GetSampleTime());
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager,
+ ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time());
+
+ apzc->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(100.0f, apzc->GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer::eForEventHandling)
+ .y);
+}
diff --git a/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp
new file mode 100644
index 0000000000..f9eeba66e6
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp
@@ -0,0 +1,569 @@
+/* -*- 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/. */
+
+#include "APZCBasicTester.h"
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "apz/util/APZEventState.h"
+
+#include "InputUtils.h"
+
+class APZCTransformNotificationTester : public APZCTreeManagerTester {
+ public:
+ explicit APZCTransformNotificationTester() { CreateMockHitTester(); }
+
+ UniquePtr<ScopedLayerTreeRegistration> mRegistration;
+
+ RefPtr<TestAsyncPanZoomController> mRootApzc;
+
+ void SetupBasicTest() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+
+ mRegistration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ mRootApzc = ApzcOf(root);
+ }
+
+ void SetupNonScrollableTest() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 100, 100));
+
+ mRegistration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+
+ mRootApzc = ApzcOf(root);
+
+ mRootApzc->GetFrameMetrics().SetIsRootContent(true);
+ }
+};
+
+TEST_F(APZCTransformNotificationTester, PanningTransformNotifications) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ SetupBasicTest();
+
+ // Scroll down by 25 px. Ensure we only get one set of
+ // state change notifications.
+ //
+ // Then, scroll back up by 20px, this time flinging after.
+ // The fling should cover the remaining 5 px of room to scroll, then
+ // go into overscroll, and finally snap-back to recover from overscroll.
+ // Again, ensure we only get one set of state change notifications for
+ // this entire procedure.
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Simple pan"));
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartPanning, _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Complex pan"));
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartPanning, _, _))
+ .Times(1);
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Simple pan");
+ Pan(mRootApzc, 50, 25, PanOptions::NoFling);
+ check.Call("Complex pan");
+ Pan(mRootApzc, 25, 45);
+ mRootApzc->AdvanceAnimationsUntilEnd();
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester, PanWithMomentumTransformNotifications) {
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ EXPECT_CALL(check, Call("Momentum Start"));
+
+ EXPECT_CALL(check, Call("Momentum Pan"));
+ EXPECT_CALL(check, Call("Momentum End"));
+ // The TransformEnd should only be sent after the momentum pan.
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Momentum Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Momentum Pan");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Momentum End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager,
+ ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester,
+ PanWithoutMomentumTransformNotifications) {
+ // Ensure that the TransformEnd delay is 100ms.
+ SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ EXPECT_CALL(check, Call("TransformEnd delay"));
+ // The TransformEnd should only be sent after the pan gesture and 100ms
+ // timer fire.
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("TransformEnd delay");
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester,
+ PanFollowedByNewPanTransformNotifications) {
+ // Ensure that the TransformEnd delay is 100ms.
+ SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ // The TransformEnd delay should be cut short and delivered before the
+ // new pan gesture begins.
+ EXPECT_CALL(check, Call("New Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("New Pan End"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("New Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("New Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(105);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester,
+ PanFollowedByWheelTransformNotifications) {
+ // Needed because the test uses SmoothWheel()
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", true);
+ // Ensure that the TransformEnd delay is 100ms.
+ SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning"));
+ EXPECT_CALL(check, Call("Pan End"));
+ // The TransformEnd delay should be cut short and delivered before the
+ // new wheel event begins.
+ EXPECT_CALL(check, Call("Wheel Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Wheel End"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, 30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(55);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Wheel Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ SmoothWheel(manager, ScreenIntPoint(50, 50), ScreenPoint(10, 10),
+ mcc->Time());
+ mcc->AdvanceByMillis(10);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Wheel End");
+
+ mRootApzc->AdvanceAnimationsUntilEnd();
+
+ check.Call("Done");
+}
+
+#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
+TEST_F(APZCTransformNotificationTester, PanOverscrollTransformNotifications) {
+ SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
+
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Pan Start"));
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformBegin, _, _))
+ .Times(1);
+
+ EXPECT_CALL(check, Call("Panning Into Overscroll"));
+ EXPECT_CALL(check, Call("Pan End"));
+ EXPECT_CALL(check, Call("Overscroll Animation End"));
+ // The TransformEnd should only be sent after the overscroll animation
+ // completes.
+ EXPECT_CALL(
+ *mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eTransformEnd, _, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Pan Start");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50),
+ ScreenIntPoint(1, 2), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Panning Into Overscroll");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50),
+ ScreenPoint(15, -30), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Ensure that we have overscrolled.
+ EXPECT_TRUE(mRootApzc->IsOverscrolled());
+
+ check.Call("Pan End");
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50),
+ ScreenPoint(0, 0), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ // Wait for the overscroll animation to complete and the TransformEnd
+ // notification to be sent.
+ check.Call("Overscroll Animation End");
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimationsUntilEnd();
+ EXPECT_FALSE(mRootApzc->IsOverscrolled());
+
+ check.Call("Done");
+}
+#endif
+
+TEST_F(APZCTransformNotificationTester, ScrollableTouchStateChange) {
+ // Create a scroll frame with available space for a scroll.
+ SetupBasicTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ EXPECT_CALL(check, Call("Start"));
+ // We receive a touch-start with the flag indicating that the
+ // touch-start occurred over a scrollable element.
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, 1, _))
+ .Times(1);
+
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, 1, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Start");
+
+ // Conduct a touch down and touch up in the scrollable element,
+ // and ensure the correct state change notifications are sent.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
+
+TEST_F(APZCTransformNotificationTester, NonScrollableTouchStateChange) {
+ // Create a non-scrollable frame with no space to scroll.
+ SetupNonScrollableTest();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ EXPECT_CALL(check, Call("Start"));
+ // We receive a touch-start with the flag indicating that the
+ // touch-start occurred over a non-scrollable element.
+ EXPECT_CALL(
+ *mcc, NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eStartTouch, 0, _))
+ .Times(1);
+
+ EXPECT_CALL(*mcc,
+ NotifyAPZStateChange(
+ _, GeckoContentController::APZStateChange::eEndTouch, 1, _))
+ .Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Start");
+
+ // Conduct a touch down and touch up in the non-scrollable element,
+ // and ensure the correct state change notifications are sent.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
+ TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->AdvanceByMillis(5);
+ mRootApzc->AdvanceAnimations(mcc->GetSampleTime());
+
+ check.Call("Done");
+}
diff --git a/gfx/layers/apz/test/gtest/TestTreeManager.cpp b/gfx/layers/apz/test/gtest/TestTreeManager.cpp
new file mode 100644
index 0000000000..8d8cda8729
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestTreeManager.cpp
@@ -0,0 +1,411 @@
+/* -*- 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/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+#include "Units.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+class APZCTreeManagerGenericTester : public APZCTreeManagerTester {
+ protected:
+ void CreateSimpleScrollingLayer() {
+ const char* treeShape = "x";
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 200, 200),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
+ CSSRect(0, 0, 500, 500));
+ }
+
+ void CreateSimpleMultiLayerTree() {
+ const char* treeShape = "x(xx)";
+ // LayerID 0 12
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 50),
+ LayerIntRect(0, 50, 100, 50),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ }
+
+ void CreatePotentiallyLeakingTree() {
+ const char* treeShape = "x(x(x(x))x(x(x)))";
+ // LayerID 0 1 2 3 4 5 6
+ CreateScrollData(treeShape);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[5],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[3],
+ ScrollableLayerGuid::START_SCROLL_ID + 2);
+ SetScrollableFrameMetrics(layers[6],
+ ScrollableLayerGuid::START_SCROLL_ID + 3);
+ }
+
+ void CreateTwoLayerTree(int32_t aRootContentLayerIndex) {
+ const char* treeShape = "x(x)";
+ // LayerID 0 1
+ LayerIntRect layerVisibleRect[] = {
+ LayerIntRect(0, 0, 100, 100),
+ LayerIntRect(0, 0, 100, 100),
+ };
+ CreateScrollData(treeShape, layerVisibleRect);
+ SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1],
+ ScrollableLayerGuid::START_SCROLL_ID + 1);
+ SetScrollHandoff(layers[1], layers[0]);
+
+ // Make layers[aRootContentLayerIndex] the root content
+ ModifyFrameMetrics(layers[aRootContentLayerIndex],
+ [](ScrollMetadata& sm, FrameMetrics& fm) {
+ fm.SetIsRootContent(true);
+ });
+ }
+};
+
+TEST_F(APZCTreeManagerGenericTester, ScrollablePaintedLayers) {
+ CreateSimpleMultiLayerTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+
+ // both layers have the same scrollId
+ SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID);
+ UpdateHitTestingTree();
+
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ // so they should have the same APZC
+ EXPECT_FALSE(HasScrollableFrameMetrics(layers[0]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+}
+
+TEST_F(APZCTreeManagerGenericTester, Bug1068268) {
+ CreatePotentiallyLeakingTree();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+
+ UpdateHitTestingTree();
+ RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
+ RefPtr<HitTestingTreeNode> node2 = root->GetFirstChild()->GetFirstChild();
+ RefPtr<HitTestingTreeNode> node5 = root->GetLastChild()->GetLastChild();
+
+ EXPECT_EQ(ApzcOf(layers[2]), node5->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[2]), node2->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[0]), ApzcOf(layers[2])->GetParent());
+ EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[5]));
+
+ EXPECT_EQ(node2->GetFirstChild(), node2->GetLastChild());
+ EXPECT_EQ(ApzcOf(layers[3]), node2->GetLastChild()->GetApzc());
+ EXPECT_EQ(node5->GetFirstChild(), node5->GetLastChild());
+ EXPECT_EQ(ApzcOf(layers[6]), node5->GetLastChild()->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[3])->GetParent());
+ EXPECT_EQ(ApzcOf(layers[5]), ApzcOf(layers[6])->GetParent());
+}
+
+class APZCTreeManagerGenericTesterMock : public APZCTreeManagerGenericTester {
+ public:
+ APZCTreeManagerGenericTesterMock() { CreateMockHitTester(); }
+};
+
+TEST_F(APZCTreeManagerGenericTesterMock, Bug1194876) {
+ // Create a layer tree with parent and child scrollable layers, with the
+ // child being the root content.
+ CreateTwoLayerTree(1);
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ uint64_t blockId;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ // First touch goes down, APZCTM will hit layers[1] because it is on top of
+ // layers[0], but we tell it the real target APZC is layers[0].
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, false);
+ targets.AppendElement(ApzcOf(layers[0])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Around here, the above touch will get processed by ApzcOf(layers[0])
+
+ // Second touch goes down (first touch remains down), APZCTM will again hit
+ // layers[1]. Again we tell it both touches landed on layers[0], but because
+ // layers[1] is the RCD layer, it will end up being the multitouch target.
+ mti.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0));
+ // Each touch will get hit-tested, so queue two hit-test results.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, false);
+ targets.AppendElement(ApzcOf(layers[0])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Around here, the above multi-touch will get processed by ApzcOf(layers[1]).
+ // We want to ensure that ApzcOf(layers[0]) has had its state cleared, because
+ // otherwise it will do things like dispatch spurious long-tap events.
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(0);
+}
+
+TEST_F(APZCTreeManagerGenericTesterMock, TargetChangesMidGesture_Bug1570559) {
+ // Create a layer tree with parent and child scrollable layers, with the
+ // parent being the root content.
+ CreateTwoLayerTree(0);
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ uint64_t blockId;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ // First touch goes down. APZCTM hits the child layer because it is on top
+ // (and we confirm this target), but do not prevent-default the event, causing
+ // the child APZC's gesture detector to start a long-tap timeout task.
+ MultiTouchInput mti =
+ CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(
+ SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0));
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, /* default prevented = */ false);
+ targets.AppendElement(ApzcOf(layers[1])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Second touch goes down (first touch remains down). APZCTM again hits the
+ // child and we confirm this, but multi-touch events are routed to the root
+ // content APZC which is the parent. This event is prevent-defaulted, so we
+ // clear the parent's gesture state. The bug is that we fail to clear the
+ // child's gesture state.
+ mti.mTouches.AppendElement(
+ SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0));
+ // Each touch will get hit-tested, so queue two hit-test results.
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, /* default prevented = */ true);
+ targets.AppendElement(ApzcOf(layers[1])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // If we've failed to clear the child's gesture state, then the long tap
+ // timeout task will fire in TearDown() and a long-tap will be dispatched.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(0);
+}
+
+TEST_F(APZCTreeManagerGenericTesterMock, Bug1198900) {
+ // This is just a test that cancels a wheel event to make sure it doesn't
+ // crash.
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ ScreenPoint origin(100, 50);
+ ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
+ ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
+ false, WheelDeltaAdjustmentStrategy::eNone);
+ uint64_t blockId;
+ QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
+ {CompositorHitTestFlags::eVisibleToHitTest,
+ CompositorHitTestFlags::eIrregularArea});
+ blockId = manager->ReceiveInputEvent(swi).mInputBlockId;
+ manager->ContentReceivedInputBlock(blockId, /* preventDefault= */ true);
+}
+
+// The next two tests check that APZ clamps the scroll offset it composites even
+// if the main thread fails to do so. (The main thread will always clamp its
+// scroll offset internally, but it may not send APZ the clamped version for
+// scroll offset synchronization reasons.)
+TEST_F(APZCTreeManagerTester, Bug1551582) {
+ // The simple layer tree has a scrollable rect of 500x500 and a composition
+ // bounds of 200x200, leading to a scroll range of (0,0,300,300).
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Simulate the main thread scrolling to the end of the scroll range.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300));
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300))));
+ aSm.SetScrollUpdates(scrollUpdates);
+ aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ });
+ UpdateHitTestingTree();
+
+ // Sanity check.
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset);
+
+ // Simulate the main thread shrinking the scrollable rect to 400x400 (and
+ // thereby the scroll range to (0,0,200,200) without sending a new scroll
+ // offset update for the clamped scroll position (200,200).
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetScrollableRect(CSSRect(0, 0, 400, 400));
+ });
+ UpdateHitTestingTree();
+
+ // Check that APZ has clamped the scroll offset to (200,200) for us.
+ compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset);
+}
+TEST_F(APZCTreeManagerTester, Bug1557424) {
+ // The simple layer tree has a scrollable rect of 500x500 and a composition
+ // bounds of 200x200, leading to a scroll range of (0,0,300,300).
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Simulate the main thread scrolling to the end of the scroll range.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300));
+ nsTArray<ScrollPositionUpdate> scrollUpdates;
+ scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
+ ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300))));
+ aSm.SetScrollUpdates(scrollUpdates);
+ aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
+ });
+ UpdateHitTestingTree();
+
+ // Sanity check.
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset);
+
+ // Simulate the main thread expanding the composition bounds to 300x300 (and
+ // thereby shrinking the scroll range to (0,0,200,200) without sending a new
+ // scroll offset update for the clamped scroll position (200,200).
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ aMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300));
+ });
+ UpdateHitTestingTree();
+
+ // Check that APZ has clamped the scroll offset to (200,200) for us.
+ compositedScrollOffset = apzc->GetCompositedScrollOffset();
+ EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset);
+}
+
+TEST_F(APZCTreeManagerTester, Bug1805601) {
+ // The simple layer tree has a scrollable rect of 500x500 and a composition
+ // bounds of 200x200, leading to a scroll range of (0,0,300,300) at unit zoom.
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+ FrameMetrics& compositorMetrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange());
+
+ // Zoom the page in by 2x. This needs to be reflected in each of the pres
+ // shell resolution, cumulative resolution, and zoom. This makes the scroll
+ // range (0,0,400,400).
+ compositorMetrics.SetZoom(CSSToParentLayerScale(2.0));
+ EXPECT_EQ(CSSRect(0, 0, 400, 400), compositorMetrics.CalculateScrollRange());
+
+ // Scroll to an area inside the 2x scroll range but outside the original one.
+ compositorMetrics.ClampAndSetVisualScrollOffset(CSSPoint(350, 350));
+ EXPECT_EQ(CSSPoint(350, 350), compositorMetrics.GetVisualScrollOffset());
+
+ // Simulate a main-thread update where the zoom is reset to 1x but the visual
+ // scroll offset is unmodified.
+ ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
+ // Changes to |compositorMetrics| are not reflected in |aMetrics|, which
+ // is the "layer tree" copy, so we don't need to explicitly set the zoom to
+ // 1.0 (it still has that as the initial value), but we do need to set
+ // the visual scroll offset to the same value the APZ copy has.
+ aMetrics.SetVisualScrollOffset(CSSPoint(350, 350));
+
+ // Needed to get APZ to accept the 1.0 zoom in |aMetrics|, otherwise
+ // it will act as though its zoom is newer (e.g. an async zoom that hasn't
+ // been repainted yet) and ignore ours.
+ aSm.SetResolutionUpdated(true);
+ });
+ UpdateHitTestingTree();
+
+ // Check that APZ clamped the scroll offset.
+ EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange());
+ EXPECT_EQ(CSSPoint(300, 300), compositorMetrics.GetVisualScrollOffset());
+}
+
+TEST_F(APZCTreeManagerTester,
+ InstantKeyScrollBetweenTwoSamplingsWithSameTimeStamp) {
+ if (!StaticPrefs::apz_keyboard_enabled_AtStartup()) {
+ // On Android apz.keyboard.enabled is false by default and it's can't be
+ // changed here since it's `mirror: once`, so we just skip this test.
+ return;
+ }
+
+ // For instant scrolling, i.e. no async animation should not be involved.
+ SCOPED_GFX_PREF_BOOL("general.smoothScroll", false);
+
+ // Set up a keyboard shortcuts map to scroll page down.
+ AutoTArray<KeyboardShortcut, 1> shortcuts{KeyboardShortcut(
+ KeyboardInput::KEY_DOWN, 0, 0, 0, 0,
+ KeyboardScrollAction(
+ KeyboardScrollAction::KeyboardScrollActionType::eScrollPage, true))};
+ KeyboardMap keyboardMap(std::move(shortcuts));
+ manager->SetKeyboardMap(keyboardMap);
+
+ // Set up a scrollable layer.
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
+ UpdateHitTestingTree();
+
+ // Setup the scrollable layer is scrollable by key events.
+ FocusTarget focusTarget;
+ focusTarget.mSequenceNumber = 1;
+ focusTarget.mData = AsVariant<FocusTarget::ScrollTargets>(
+ {ScrollableLayerGuid::START_SCROLL_ID,
+ ScrollableLayerGuid::START_SCROLL_ID});
+ manager->UpdateFocusState(LayersId{0}, LayersId{0}, focusTarget);
+
+ // A vsync tick happens.
+ mcc->AdvanceByMillis(16);
+
+ // The first sampling happens, there's no change have happened, thus no need
+ // to composite.
+ EXPECT_FALSE(manager->AdvanceAnimations(mcc->GetSampleTime()));
+
+ // A key event causing scroll page down happens.
+ WidgetKeyboardEvent widgetEvent(true, eKeyDown, nullptr);
+ KeyboardInput input(widgetEvent);
+ Unused << manager->ReceiveInputEvent(input);
+
+ // Simulate WebRender compositing frames until APZ tells it the scroll offset
+ // has stopped changing.
+ // Important to trigger the bug: the first composite has the same time stamp
+ // as the earlier one above.
+ ParentLayerPoint compositedScrollOffset;
+ while (true) {
+ bool needMoreFrames = manager->AdvanceAnimations(mcc->GetSampleTime());
+ compositedScrollOffset = ApzcOf(root)->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForCompositing);
+ if (!needMoreFrames) {
+ break;
+ }
+ mcc->AdvanceBy(TimeDuration::FromMilliseconds(16));
+ }
+
+ // Check that the effect of the keyboard scroll has been composited.
+ EXPECT_GT(compositedScrollOffset.y, 0);
+}
diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.cpp b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp
new file mode 100644
index 0000000000..6d07a91a24
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp
@@ -0,0 +1,273 @@
+/* -*- 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/. */
+
+#include "TestWRScrollData.h"
+#include "APZTestAccess.h"
+#include "gtest/gtest.h"
+#include "FrameMetrics.h"
+#include "gfxPlatform.h"
+#include "mozilla/layers/APZUpdater.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "mozilla/UniquePtr.h"
+#include "apz/src/APZCTreeManager.h"
+
+using mozilla::layers::APZCTreeManager;
+using mozilla::layers::APZUpdater;
+using mozilla::layers::LayersId;
+using mozilla::layers::ScrollableLayerGuid;
+using mozilla::layers::ScrollMetadata;
+using mozilla::layers::TestWRScrollData;
+using mozilla::layers::WebRenderLayerScrollData;
+using mozilla::layers::WebRenderScrollDataWrapper;
+
+/* static */
+TestWRScrollData TestWRScrollData::Create(const char* aTreeShape,
+ const APZUpdater& aUpdater,
+ const LayerIntRect* aVisibleRects,
+ const gfx::Matrix4x4* aTransforms) {
+ // The WebRenderLayerScrollData tree needs to be created in a fairly
+ // particular way (for example, each node needs to know the number of
+ // descendants it has), so this function takes care to create the nodes
+ // in the same order as WebRenderCommandBuilder would.
+ TestWRScrollData result;
+ const size_t len = strlen(aTreeShape);
+ // "Layer index" in this function refers to the index by which a layer will
+ // be accessible via TestWRScrollData::GetLayer(), and matches the order
+ // in which the layer appears in |aTreeShape|.
+ size_t currentLayerIndex = 0;
+ struct LayerEntry {
+ size_t mLayerIndex;
+ int32_t mDescendantCount = 0;
+ };
+ // Layers we have encountered in |aTreeShape|, but have not built a
+ // WebRenderLayerScrollData for. (It can only be built after its
+ // descendants have been encountered and counted.)
+ std::stack<LayerEntry> pendingLayers;
+ std::vector<WebRenderLayerScrollData> finishedLayers;
+ // Tracks the level of nesting of '(' characters. Starts at 1 to account
+ // for the root layer.
+ size_t depth = 1;
+ // Helper function for finishing a layer once all its descendants have been
+ // encountered.
+ auto finishLayer = [&] {
+ MOZ_ASSERT(!pendingLayers.empty());
+ LayerEntry entry = pendingLayers.top();
+
+ WebRenderLayerScrollData layer;
+ APZTestAccess::InitializeForTest(layer, entry.mDescendantCount);
+ if (aVisibleRects) {
+ layer.SetVisibleRect(aVisibleRects[entry.mLayerIndex]);
+ }
+ if (aTransforms) {
+ layer.SetTransform(aTransforms[entry.mLayerIndex]);
+ }
+ finishedLayers.push_back(std::move(layer));
+
+ // |finishedLayers| stores the layers in a different order than they
+ // appeared in |aTreeShape|. To be able to access layers by their layer
+ // index, keep a mapping from layer index to index in |finishedLayers|.
+ result.mIndexMap.emplace(entry.mLayerIndex, finishedLayers.size() - 1);
+
+ pendingLayers.pop();
+
+ // Keep track of descendant counts. The +1 is for the layer just finished.
+ if (!pendingLayers.empty()) {
+ pendingLayers.top().mDescendantCount += (entry.mDescendantCount + 1);
+ }
+ };
+ for (size_t i = 0; i < len; ++i) {
+ if (aTreeShape[i] == '(') {
+ ++depth;
+ } else if (aTreeShape[i] == ')') {
+ if (pendingLayers.size() <= 1) {
+ printf("Invalid tree shape: too many ')'\n");
+ MOZ_CRASH();
+ }
+ finishLayer(); // finish last layer at current depth
+ --depth;
+ } else {
+ if (aTreeShape[i] != 'x') {
+ printf("The only allowed character to represent a layer is 'x'\n");
+ MOZ_CRASH();
+ }
+ if (depth == pendingLayers.size()) {
+ // We have a previous layer at this same depth to finish.
+ if (depth <= 1) {
+ printf("The tree is only allowed to have one root\n");
+ MOZ_CRASH();
+ }
+ finishLayer();
+ }
+ MOZ_ASSERT(depth == pendingLayers.size() + 1);
+ pendingLayers.push({currentLayerIndex});
+ ++currentLayerIndex;
+ }
+ }
+ if (pendingLayers.size() != 1) {
+ printf("Invalid tree shape: '(' and ')' not balanced\n");
+ MOZ_CRASH();
+ }
+ finishLayer(); // finish root layer
+
+ // As in WebRenderCommandBuilder, the layers need to be added to the
+ // WebRenderScrollData in reverse of the order in which they were built.
+ for (auto it = finishedLayers.rbegin(); it != finishedLayers.rend(); ++it) {
+ result.AddLayerData(std::move(*it));
+ }
+ // mIndexMap also needs to be adjusted to accout for the reversal above.
+ for (auto& [layerIndex, storedIndex] : result.mIndexMap) {
+ (void)layerIndex; // suppress -Werror=unused-variable
+ storedIndex = result.GetLayerCount() - storedIndex - 1;
+ }
+
+ return result;
+}
+
+const WebRenderLayerScrollData* TestWRScrollData::operator[](
+ size_t aLayerIndex) const {
+ auto it = mIndexMap.find(aLayerIndex);
+ if (it == mIndexMap.end()) {
+ return nullptr;
+ }
+ return GetLayerData(it->second);
+}
+
+WebRenderLayerScrollData* TestWRScrollData::operator[](size_t aLayerIndex) {
+ auto it = mIndexMap.find(aLayerIndex);
+ if (it == mIndexMap.end()) {
+ return nullptr;
+ }
+ return GetLayerData(it->second);
+}
+
+void TestWRScrollData::SetScrollMetadata(
+ size_t aLayerIndex, const nsTArray<ScrollMetadata>& aMetadata) {
+ WebRenderLayerScrollData* layer = operator[](aLayerIndex);
+ MOZ_ASSERT(layer);
+ for (const ScrollMetadata& metadata : aMetadata) {
+ layer->AppendScrollMetadata(*this, metadata);
+ }
+}
+
+class WebRenderScrollDataWrapperTester : public ::testing::Test {
+ protected:
+ virtual void SetUp() {
+ // This ensures ScrollMetadata::sNullMetadata is initialized.
+ gfxPlatform::GetPlatform();
+
+ mManager = APZCTreeManager::Create(LayersId{0});
+ mUpdater = new APZUpdater(mManager, false);
+ }
+
+ RefPtr<APZCTreeManager> mManager;
+ RefPtr<APZUpdater> mUpdater;
+};
+
+TEST_F(WebRenderScrollDataWrapperTester, SimpleTree) {
+ auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater);
+ WebRenderScrollDataWrapper w0(*mUpdater, &layers);
+
+ ASSERT_EQ(layers[0], w0.GetLayer());
+ WebRenderScrollDataWrapper w1 = w0.GetLastChild();
+ ASSERT_EQ(layers[1], w1.GetLayer());
+ ASSERT_FALSE(w1.GetPrevSibling().IsValid());
+ WebRenderScrollDataWrapper w5 = w1.GetLastChild();
+ ASSERT_EQ(layers[5], w5.GetLayer());
+ WebRenderScrollDataWrapper w6 = w5.GetLastChild();
+ ASSERT_EQ(layers[6], w6.GetLayer());
+ ASSERT_FALSE(w6.GetLastChild().IsValid());
+ WebRenderScrollDataWrapper w2 = w5.GetPrevSibling();
+ ASSERT_EQ(layers[2], w2.GetLayer());
+ ASSERT_FALSE(w2.GetPrevSibling().IsValid());
+ WebRenderScrollDataWrapper w4 = w2.GetLastChild();
+ ASSERT_EQ(layers[4], w4.GetLayer());
+ ASSERT_FALSE(w4.GetLastChild().IsValid());
+ WebRenderScrollDataWrapper w3 = w4.GetPrevSibling();
+ ASSERT_EQ(layers[3], w3.GetLayer());
+ ASSERT_FALSE(w3.GetLastChild().IsValid());
+ ASSERT_FALSE(w3.GetPrevSibling().IsValid());
+}
+
+static ScrollMetadata MakeMetadata(ScrollableLayerGuid::ViewID aId) {
+ ScrollMetadata metadata;
+ metadata.GetMetrics().SetScrollId(aId);
+ return metadata;
+}
+
+TEST_F(WebRenderScrollDataWrapperTester, MultiFramemetricsTree) {
+ auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater);
+
+ nsTArray<ScrollMetadata> metadata;
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID +
+ 0)); // topmost of root layer
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 1));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 2));
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(
+ ScrollableLayerGuid::NULL_SCROLL_ID)); // bottom of root layer
+ layers.SetScrollMetadata(0, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 3));
+ layers.SetScrollMetadata(1, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 4));
+ layers.SetScrollMetadata(2, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 5));
+ layers.SetScrollMetadata(4, metadata);
+
+ metadata.Clear();
+ metadata.InsertElementAt(0,
+ MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID));
+ metadata.InsertElementAt(
+ 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 6));
+ layers.SetScrollMetadata(5, metadata);
+
+ WebRenderScrollDataWrapper wrapper(*mUpdater, &layers);
+ nsTArray<WebRenderLayerScrollData*> expectedLayers;
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[0]);
+ expectedLayers.AppendElement(layers[1]);
+ expectedLayers.AppendElement(layers[5]);
+ expectedLayers.AppendElement(layers[5]);
+ expectedLayers.AppendElement(layers[6]);
+ nsTArray<ScrollableLayerGuid::ViewID> expectedIds;
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 0);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 1);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 2);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 3);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 6);
+ expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(expectedLayers[i], wrapper.GetLayer());
+ ASSERT_EQ(expectedIds[i], wrapper.Metrics().GetScrollId());
+ wrapper = wrapper.GetLastChild();
+ }
+ ASSERT_FALSE(wrapper.IsValid());
+}
diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.h b/gfx/layers/apz/test/gtest/TestWRScrollData.h
new file mode 100644
index 0000000000..c0a6a78e3a
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestWRScrollData.h
@@ -0,0 +1,63 @@
+/* -*- 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_TestWRScrollData_h
+#define mozilla_layers_TestWRScrollData_h
+
+#include "mozilla/gfx/MatrixFwd.h"
+#include "mozilla/layers/WebRenderScrollData.h"
+
+namespace mozilla {
+namespace layers {
+
+class APZUpdater;
+
+// Extends WebRenderScrollData with some methods useful for gtests.
+class TestWRScrollData : public WebRenderScrollData {
+ public:
+ TestWRScrollData() = default;
+ TestWRScrollData(TestWRScrollData&& aOther) = default;
+ TestWRScrollData& operator=(TestWRScrollData&& aOther) = default;
+
+ /*
+ * Create a WebRenderLayerScrollData tree described by |aTreeShape|.
+ * |aTreeShape| is expected to be a string where each character is
+ * either 'x' to indicate a node in the tree, or a '(' or ')' to indicate
+ * the start/end of a subtree.
+ *
+ * Example "x(x(x(xx)x))" would yield:
+ * x
+ * |
+ * x
+ * / \
+ * x x
+ * / \
+ * x x
+ *
+ * The caller may optionally provide visible rects and/or transforms
+ * for the nodes. If provided, the array should contain one element
+ * for each node, in the same order as in |aTreeShape|.
+ */
+ static TestWRScrollData Create(const char* aTreeShape,
+ const APZUpdater& aUpdater,
+ const LayerIntRect* aVisibleRects = nullptr,
+ const gfx::Matrix4x4* aTransforms = nullptr);
+
+ // These methods allow accessing and manipulating layers based on an index
+ // representing the order in which they appear in |aTreeShape|.
+ WebRenderLayerScrollData* operator[](size_t aLayerIndex);
+ const WebRenderLayerScrollData* operator[](size_t aLayerIndex) const;
+ void SetScrollMetadata(size_t aLayerIndex,
+ const nsTArray<ScrollMetadata>& aMetadata);
+
+ private:
+ std::map<size_t, size_t> mIndexMap; // Used to implement GetLayer()
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build
new file mode 100644
index 0000000000..5c9231fd21
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "APZTestAccess.cpp",
+ "APZTestCommon.cpp",
+ "MockHitTester.cpp",
+ "TestAxisLock.cpp",
+ "TestBasic.cpp",
+ "TestEventRegions.cpp",
+ "TestEventResult.cpp",
+ "TestFlingAcceleration.cpp",
+ "TestGestureDetector.cpp",
+ "TestHitTesting.cpp",
+ "TestInputQueue.cpp",
+ "TestOverscroll.cpp",
+ "TestPanning.cpp",
+ "TestPinching.cpp",
+ "TestPointerEventsConsumable.cpp",
+ "TestScrollbarDragging.cpp",
+ "TestScrollHandoff.cpp",
+ "TestSnapping.cpp",
+ "TestSnappingOnMomentum.cpp",
+ "TestTransformNotifications.cpp",
+ "TestTreeManager.cpp",
+ "TestWRScrollData.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/gfx/2d",
+ "/gfx/cairo/cairo/src",
+ "/gfx/layers",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp
new file mode 100644
index 0000000000..e5b2dc7af8
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp
@@ -0,0 +1,221 @@
+/* -*- 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/. */
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+#include <functional>
+
+#include "MobileViewportManager.h"
+#include "mozilla/MVMContext.h"
+#include "mozilla/dom/Event.h"
+
+using namespace mozilla;
+
+class MockMVMContext : public MVMContext {
+ using AutoSizeFlag = nsViewportInfo::AutoSizeFlag;
+ using AutoScaleFlag = nsViewportInfo::AutoScaleFlag;
+ using ZoomFlag = nsViewportInfo::ZoomFlag;
+
+ // A "layout function" is a function that computes the content size
+ // as a function of the ICB size.
+ using LayoutFunction = std::function<CSSSize(CSSSize aICBSize)>;
+
+ public:
+ // MVMContext methods we don't care to implement.
+ MOCK_METHOD3(AddEventListener,
+ void(const nsAString& aType, nsIDOMEventListener* aListener,
+ bool aUseCapture));
+ MOCK_METHOD3(RemoveEventListener,
+ void(const nsAString& aType, nsIDOMEventListener* aListener,
+ bool aUseCapture));
+ MOCK_METHOD3(AddObserver, void(nsIObserver* aObserver, const char* aTopic,
+ bool aOwnsWeak));
+ MOCK_METHOD2(RemoveObserver,
+ void(nsIObserver* aObserver, const char* aTopic));
+ MOCK_METHOD0(Destroy, void());
+
+ MOCK_METHOD1(SetVisualViewportSize, void(const CSSSize& aSize));
+ MOCK_METHOD0(PostVisualViewportResizeEventByDynamicToolbar, void());
+ MOCK_METHOD0(UpdateDisplayPortMargins, void());
+ MOCK_METHOD0(GetDynamicToolbarOffset, ScreenIntCoord());
+
+ void SetMVM(MobileViewportManager* aMVM) { mMVM = aMVM; }
+
+ // MVMContext method implementations.
+ nsViewportInfo GetViewportInfo(const ScreenIntSize& aDisplaySize) const {
+ // This is a very basic approximation of what Document::GetViewportInfo()
+ // does in the most common cases.
+ // Ideally, we would invoke the algorithm in Document::GetViewportInfo()
+ // itself, but that would require refactoring it a bit to remove
+ // dependencies on the actual Document which we don't have available in
+ // this test harness.
+ CSSSize viewportSize = mDisplaySize / mDeviceScale;
+ if (mAutoSizeFlag == AutoSizeFlag::FixedSize) {
+ viewportSize = CSSSize(mFixedViewportWidth,
+ mFixedViewportWidth * (float(mDisplaySize.height) /
+ mDisplaySize.width));
+ }
+ return nsViewportInfo(mDefaultScale, mMinScale, mMaxScale, viewportSize,
+ mAutoSizeFlag, mAutoScaleFlag, mZoomFlag,
+ dom::ViewportFitType::Auto);
+ }
+ CSSToLayoutDeviceScale CSSToDevPixelScale() const { return mDeviceScale; }
+ float GetResolution() const { return mResolution; }
+ bool SubjectMatchesDocument(nsISupports* aSubject) const { return true; }
+ Maybe<CSSRect> CalculateScrollableRectForRSF() const {
+ return Some(CSSRect(CSSPoint(), mContentSize));
+ }
+ bool IsResolutionUpdatedByApz() const { return false; }
+ LayoutDeviceMargin ScrollbarAreaToExcludeFromCompositionBounds() const {
+ return LayoutDeviceMargin();
+ }
+ Maybe<LayoutDeviceIntSize> GetDocumentViewerSize() const {
+ return Some(mDisplaySize);
+ }
+ bool AllowZoomingForDocument() const { return true; }
+ bool IsInReaderMode() const { return false; }
+ bool IsDocumentLoading() const { return false; }
+
+ void SetResolutionAndScaleTo(float aResolution,
+ ResolutionChangeOrigin aOrigin) {
+ mResolution = aResolution;
+ mMVM->ResolutionUpdated(aOrigin);
+ }
+ void Reflow(const CSSSize& aNewSize) {
+ mICBSize = aNewSize;
+ mContentSize = mLayoutFunction(mICBSize);
+ }
+
+ // Allow test code to modify the input metrics.
+ void SetMinScale(CSSToScreenScale aMinScale) { mMinScale = aMinScale; }
+ void SetMaxScale(CSSToScreenScale aMaxScale) { mMaxScale = aMaxScale; }
+ void SetInitialScale(CSSToScreenScale aInitialScale) {
+ mDefaultScale = aInitialScale;
+ mAutoScaleFlag = AutoScaleFlag::FixedScale;
+ }
+ void SetFixedViewportWidth(CSSCoord aWidth) {
+ mFixedViewportWidth = aWidth;
+ mAutoSizeFlag = AutoSizeFlag::FixedSize;
+ }
+ void SetDisplaySize(const LayoutDeviceIntSize& aNewDisplaySize) {
+ mDisplaySize = aNewDisplaySize;
+ }
+ void SetLayoutFunction(const LayoutFunction& aLayoutFunction) {
+ mLayoutFunction = aLayoutFunction;
+ }
+
+ // Allow test code to query the output metrics.
+ CSSSize GetICBSize() const { return mICBSize; }
+ CSSSize GetContentSize() const { return mContentSize; }
+
+ private:
+ // Input metrics, with some sensible defaults.
+ LayoutDeviceIntSize mDisplaySize{300, 600};
+ CSSToScreenScale mDefaultScale{1.0f};
+ CSSToScreenScale mMinScale{0.25f};
+ CSSToScreenScale mMaxScale{10.0f};
+ CSSToLayoutDeviceScale mDeviceScale{1.0f};
+ CSSCoord mFixedViewportWidth;
+ AutoSizeFlag mAutoSizeFlag = AutoSizeFlag::AutoSize;
+ AutoScaleFlag mAutoScaleFlag = AutoScaleFlag::AutoScale;
+ ZoomFlag mZoomFlag = ZoomFlag::AllowZoom;
+ // As a default layout function, just set the content size to the ICB size.
+ LayoutFunction mLayoutFunction = [](CSSSize aICBSize) { return aICBSize; };
+
+ // Output metrics.
+ float mResolution = 1.0f;
+ CSSSize mICBSize;
+ CSSSize mContentSize;
+
+ MobileViewportManager* mMVM = nullptr;
+};
+
+class MVMTester : public ::testing::Test {
+ public:
+ MVMTester()
+ : mMVMContext(new MockMVMContext()),
+ mMVM(new MobileViewportManager(
+ mMVMContext,
+ MobileViewportManager::ManagerType::VisualAndMetaViewport)) {
+ mMVMContext->SetMVM(mMVM.get());
+ }
+
+ void Resize(const LayoutDeviceIntSize& aNewDisplaySize) {
+ mMVMContext->SetDisplaySize(aNewDisplaySize);
+ mMVM->RequestReflow(false);
+ }
+
+ protected:
+ RefPtr<MockMVMContext> mMVMContext;
+ RefPtr<MobileViewportManager> mMVM;
+};
+
+TEST_F(MVMTester, ZoomBoundsRespectedAfterRotation_Bug1536755) {
+ // Set up initial conditions.
+ mMVMContext->SetDisplaySize(LayoutDeviceIntSize(600, 300));
+ mMVMContext->SetInitialScale(CSSToScreenScale(1.0f));
+ mMVMContext->SetMinScale(CSSToScreenScale(1.0f));
+ mMVMContext->SetMaxScale(CSSToScreenScale(1.0f));
+ // Set a layout function that simulates a page which is twice
+ // as tall as it is wide.
+ mMVMContext->SetLayoutFunction([](CSSSize aICBSize) {
+ return CSSSize(aICBSize.width, aICBSize.width * 2);
+ });
+
+ // Perform an initial viewport computation and reflow, and
+ // sanity-check the results.
+ mMVM->SetInitialViewport();
+ EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(1.0f, mMVMContext->GetResolution());
+
+ // Now rotate the screen, and check that the minimum and maximum
+ // scales are still respected after the rotation.
+ Resize(LayoutDeviceIntSize(300, 600));
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetContentSize());
+ EXPECT_EQ(1.0f, mMVMContext->GetResolution());
+}
+
+TEST_F(MVMTester, LandscapeToPortraitRotation_Bug1523844) {
+ // Set up initial conditions.
+ mMVMContext->SetDisplaySize(LayoutDeviceIntSize(300, 600));
+ // Set a layout function that simulates a page with a fixed
+ // content size that's as wide as the screen in one orientation
+ // (and wider in the other).
+ mMVMContext->SetLayoutFunction(
+ [](CSSSize aICBSize) { return CSSSize(600, 1200); });
+
+ // Simulate a "DOMMetaAdded" event being fired before calling
+ // SetInitialViewport(). This matches what typically happens
+ // during real usage (the MVM receives the "DOMMetaAdded"
+ // before the "load", and it's the "load" that calls
+ // SetInitialViewport()), and is important to trigger this
+ // bug, because it causes the MVM to be stuck with an
+ // "mRestoreResolution" (prior to the fix).
+ mMVM->HandleDOMMetaAdded();
+
+ // Perform an initial viewport computation and reflow, and
+ // sanity-check the results.
+ mMVM->SetInitialViewport();
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(0.5f, mMVMContext->GetResolution());
+
+ // Rotate to landscape.
+ Resize(LayoutDeviceIntSize(600, 300));
+ EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(1.0f, mMVMContext->GetResolution());
+
+ // Rotate back to portrait and check that we have returned
+ // to the portrait resolution.
+ Resize(LayoutDeviceIntSize(300, 600));
+ EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize());
+ EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize());
+ EXPECT_EQ(0.5f, mMVMContext->GetResolution());
+}
diff --git a/gfx/layers/apz/test/gtest/mvm/moz.build b/gfx/layers/apz/test/gtest/mvm/moz.build
new file mode 100644
index 0000000000..0fa985307b
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/mvm/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES += [
+ "TestMobileViewportManager.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"