/* -*- 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 class APZEventResultTester : public APZCTreeManagerTester { protected: UniquePtr 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 apzc = ApzcOf(root); ScrollMetadata metadata = apzc->GetScrollMetadata(); metadata.GetMetrics().SetLayoutScrollOffset(aPoint); nsTArray 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"; LayerIntRegion layerVisibleRegions[] = { LayerIntRect(0, 0, 100, 100), }; CreateScrollData(treeShape, layerVisibleRegions); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 200, 200)); ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { metrics.SetIsRootContent(true); }); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); } enum class PreventDefaultFlag { No, Yes }; std::tuple 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"; LayerIntRegion layerVisibleRegions[] = { LayerIntRect(0, 0, 100, 100), }; CreateScrollData(treeShape, layerVisibleRegions); 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(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, ScrollDirections()})); // 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})); }