| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/input/fling_controller.h" |
| |
| #include "base/rand_util.h" |
| #include "base/run_loop.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/test/task_environment.h" |
| #include "build/build_config.h" |
| #include "build/chromecast_buildflags.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/blink/fling_booster.h" |
| #include "ui/events/gestures/physics_based_fling_curve.h" |
| |
| using blink::WebGestureEvent; |
| using blink::WebInputEvent; |
| using blink::WebMouseWheelEvent; |
| using ui::PhysicsBasedFlingCurve; |
| |
| namespace { |
| constexpr double kFrameDelta = 1000.0 / 60.0; |
| } // namespace |
| |
| namespace input { |
| |
| class FakeFlingController : public FlingController { |
| public: |
| FakeFlingController(FlingControllerEventSenderClient* event_sender_client, |
| FlingControllerSchedulerClient* scheduler_client, |
| const Config& config) |
| : FlingController(event_sender_client, scheduler_client, config) {} |
| }; |
| |
| class FlingControllerTest : public FlingControllerEventSenderClient, |
| public FlingControllerSchedulerClient, |
| public testing::TestWithParam<bool> { |
| public: |
| // testing::Test |
| FlingControllerTest() |
| : needs_begin_frame_for_fling_progress_(GetParam()), |
| task_environment_(base::test::TaskEnvironment::MainThreadType::UI) {} |
| |
| FlingControllerTest(const FlingControllerTest&) = delete; |
| FlingControllerTest& operator=(const FlingControllerTest&) = delete; |
| |
| ~FlingControllerTest() override {} |
| |
| void SetUp() override { |
| fling_controller_ = std::make_unique<FakeFlingController>( |
| this, this, FlingController::Config()); |
| fling_controller_->set_clock_for_testing(&mock_clock_); |
| AdvanceTime(); |
| } |
| |
| // FlingControllerEventSenderClient |
| void SendGeneratedWheelEvent( |
| const MouseWheelEventWithLatencyInfo& wheel_event) override { |
| wheel_event_count_++; |
| last_sent_wheel_ = wheel_event.event; |
| first_wheel_event_sent_ = true; |
| |
| if (wheel_event.event.momentum_phase == WebMouseWheelEvent::kPhaseEnded) |
| first_wheel_event_sent_ = false; |
| } |
| void SendGeneratedGestureScrollEvents( |
| const GestureEventWithLatencyInfo& gesture_event) override { |
| fling_controller_->ObserveAndMaybeConsumeGestureEvent(gesture_event); |
| sent_scroll_gesture_count_++; |
| last_sent_gesture_ = gesture_event.event; |
| } |
| |
| gfx::Size GetRootWidgetViewportSize() override { |
| return gfx::Size(1920, 1080); |
| } |
| |
| // FlingControllerSchedulerClient |
| void ScheduleFlingProgress( |
| base::WeakPtr<FlingController> fling_controller) override { |
| DCHECK(!scheduled_next_fling_progress_); |
| scheduled_next_fling_progress_ = true; |
| } |
| void DidStopFlingingOnBrowser( |
| base::WeakPtr<FlingController> fling_controller) override { |
| notified_client_after_fling_stop_ = true; |
| } |
| bool NeedsBeginFrameForFlingProgress() override { |
| return needs_begin_frame_for_fling_progress_; |
| } |
| bool ShouldUseMobileFlingCurve() override { |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS) |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| gfx::Vector2dF GetPixelsPerInch( |
| const gfx::PointF& position_in_screen) override { |
| return gfx::Vector2dF(kDefaultPixelsPerInch, kDefaultPixelsPerInch); |
| } |
| |
| void SimulateFlingStart(blink::WebGestureDevice source_device, |
| const gfx::Vector2dF& velocity, |
| bool wait_before_processing = true) { |
| scheduled_next_fling_progress_ = false; |
| sent_scroll_gesture_count_ = 0; |
| WebGestureEvent fling_start(WebInputEvent::Type::kGestureFlingStart, 0, |
| NowTicks(), source_device); |
| fling_start.data.fling_start.velocity_x = velocity.x(); |
| fling_start.data.fling_start.velocity_y = velocity.y(); |
| GestureEventWithLatencyInfo fling_start_with_latency(fling_start); |
| if (wait_before_processing) { |
| // Wait for up to one frame before processing the event. |
| AdvanceTime(base::RandInt(0, static_cast<int>(kFrameDelta))); |
| } |
| fling_controller_->ObserveAndMaybeConsumeGestureEvent( |
| fling_start_with_latency); |
| } |
| |
| void SimulateScrollBegin(blink::WebGestureDevice source_device, |
| const gfx::Vector2dF& delta) { |
| WebGestureEvent scroll_begin(WebInputEvent::Type::kGestureScrollBegin, 0, |
| NowTicks(), source_device); |
| scroll_begin.data.scroll_begin.delta_x_hint = delta.x(); |
| scroll_begin.data.scroll_begin.delta_y_hint = delta.y(); |
| scroll_begin.data.scroll_begin.inertial_phase = |
| WebGestureEvent::InertialPhaseState::kNonMomentum; |
| scroll_begin.data.scroll_begin.delta_hint_units = |
| ui::ScrollGranularity::kScrollByPrecisePixel; |
| GestureEventWithLatencyInfo scroll_begin_with_latency(scroll_begin); |
| |
| fling_controller_->ObserveAndMaybeConsumeGestureEvent( |
| scroll_begin_with_latency); |
| } |
| |
| void SimulateScrollUpdate(blink::WebGestureDevice source_device, |
| const gfx::Vector2dF& delta) { |
| WebGestureEvent scroll_update(WebInputEvent::Type::kGestureScrollUpdate, 0, |
| NowTicks(), source_device); |
| scroll_update.data.scroll_update.delta_x = delta.x(); |
| scroll_update.data.scroll_update.delta_y = delta.y(); |
| scroll_update.data.scroll_update.velocity_x = delta.x(); |
| scroll_update.data.scroll_update.velocity_y = delta.y(); |
| scroll_update.data.scroll_update.inertial_phase = |
| WebGestureEvent::InertialPhaseState::kNonMomentum; |
| scroll_update.data.scroll_update.delta_units = |
| ui::ScrollGranularity::kScrollByPrecisePixel; |
| GestureEventWithLatencyInfo scroll_update_with_latency( |
| scroll_update); |
| |
| fling_controller_->ObserveAndMaybeConsumeGestureEvent( |
| scroll_update_with_latency); |
| } |
| |
| void SimulateFlingCancel(blink::WebGestureDevice source_device) { |
| notified_client_after_fling_stop_ = false; |
| WebGestureEvent fling_cancel(WebInputEvent::Type::kGestureFlingCancel, 0, |
| NowTicks(), source_device); |
| // autoscroll fling cancel doesn't allow fling boosting. |
| if (source_device == blink::WebGestureDevice::kSyntheticAutoscroll) |
| fling_cancel.data.fling_cancel.prevent_boosting = true; |
| GestureEventWithLatencyInfo fling_cancel_with_latency(fling_cancel); |
| fling_controller_->ObserveAndMaybeConsumeGestureEvent( |
| fling_cancel_with_latency); |
| } |
| |
| void ProgressFling(base::TimeTicks current_time) { |
| DCHECK(scheduled_next_fling_progress_); |
| scheduled_next_fling_progress_ = false; |
| fling_controller_->ProgressFling(current_time); |
| } |
| |
| bool FlingInProgress() { return fling_controller_->fling_in_progress(); } |
| |
| void AdvanceTime(double time_delta_ms = kFrameDelta) { |
| mock_clock_.Advance(base::Milliseconds(time_delta_ms)); |
| } |
| |
| base::TimeTicks NowTicks() const { return mock_clock_.NowTicks(); } |
| |
| float CompleteFlingAndAccumulateScrollDelta() { |
| float total_scroll_delta = 0.f; |
| |
| // Sometimes SendGeneratedGestureScrollEvents is not run because fling is |
| // not advanced. This is due to the first |FlingScheduler::OnAnimationStep| |
| // call having the time of the last frame before AddAnimationObserver. |
| // Please see comment in |FlingController::ProgressFling|. This leaves the |
| // last_sent_gesture as a GSE. We therefore don't accrue delta in this case |
| if (last_sent_gesture_.GetType() != |
| WebInputEvent::Type::kGestureScrollEnd) { |
| DCHECK(last_sent_gesture_.GetType() == |
| WebInputEvent::Type::kGestureScrollUpdate); |
| total_scroll_delta += last_sent_gesture_.data.scroll_update.delta_x; |
| } |
| |
| while (true) { |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| if (last_sent_gesture_.GetType() == |
| WebInputEvent::Type::kGestureScrollEnd) { |
| break; |
| } else { |
| DCHECK(last_sent_gesture_.GetType() == |
| WebInputEvent::Type::kGestureScrollUpdate); |
| total_scroll_delta += last_sent_gesture_.data.scroll_update.delta_x; |
| } |
| } |
| |
| return total_scroll_delta; |
| } |
| |
| protected: |
| std::unique_ptr<FakeFlingController> fling_controller_; |
| int wheel_event_count_ = 0; |
| WebMouseWheelEvent last_sent_wheel_; |
| WebGestureEvent last_sent_gesture_; |
| bool scheduled_next_fling_progress_ = false; |
| bool notified_client_after_fling_stop_ = false; |
| bool first_wheel_event_sent_ = false; |
| int sent_scroll_gesture_count_ = 0; |
| |
| private: |
| base::SimpleTestTickClock mock_clock_; |
| |
| // This determines whether the platform ticks fling animations using |
| // SetNeedsBeginFrame (i.e. WebView). If true, we should avoid calling |
| // ProgressFling immediately after a FlingStart since this will match the |
| // behavior in FlingController::ProcessGestureFlingStart. See |
| // https://crrev.com/c/1181521. |
| bool needs_begin_frame_for_fling_progress_; |
| base::test::TaskEnvironment task_environment_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, FlingControllerTest, testing::Bool()); |
| |
| TEST_P(FlingControllerTest, |
| ControllerSendsWheelEndOnTouchpadFlingWithZeroVelocity) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, gfx::Vector2dF()); |
| // The controller doesn't start a fling and sends a wheel end event |
| // immediately. |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseEnded, last_sent_wheel_.momentum_phase); |
| EXPECT_EQ(0.f, last_sent_wheel_.delta_x); |
| EXPECT_EQ(0.f, last_sent_wheel_.delta_y); |
| } |
| |
| TEST_P(FlingControllerTest, |
| ControllerSendsGSEOnTouchscreenFlingWithZeroVelocity) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, gfx::Vector2dF()); |
| // The controller doesn't start a fling and sends a GSE immediately. |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_EQ(WebInputEvent::Type::kGestureScrollEnd, |
| last_sent_gesture_.GetType()); |
| } |
| |
| TEST_P(FlingControllerTest, ControllerHandlesTouchpadGestureFling) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| // Processing GFS will send the first fling progress event if the time delta |
| // between the timestamp of the GFS and the time that ProcessGestureFlingStart |
| // is called is large enough. |
| bool process_GFS_sent_first_event = first_wheel_event_sent_; |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| |
| if (!process_GFS_sent_first_event) { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseBegan, last_sent_wheel_.momentum_phase); |
| } else { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, |
| last_sent_wheel_.momentum_phase); |
| } |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| |
| // The rest of the wheel events must have momentum_phase == KPhaseChanged. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, last_sent_wheel_.momentum_phase); |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchpad); |
| EXPECT_FALSE(FlingInProgress()); |
| } |
| |
| // Ensure that the start time of a fling is measured from the last received |
| // GSU. This ensures that the first progress fling during FlingStart should |
| // send significant delta. If we're using the FlingStart as the start time, we |
| // would send none or very little delta. |
| TEST_P(FlingControllerTest, FlingStartsAtLastScrollUpdate) { |
| SimulateScrollUpdate(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| double time_to_advance_ms = 30.0; |
| AdvanceTime(time_to_advance_ms); |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0), /*wait_before_processing=*/false); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| if (NeedsBeginFrameForFlingProgress()) |
| ProgressFling(NowTicks()); |
| |
| // We haven't advanced time since the FlingStart. Ensure we still send a |
| // significant amount of delta (~0.030sec * 1000pixels/sec) since we should |
| // be measuring the time since the last GSU. |
| EXPECT_EQ(1, sent_scroll_gesture_count_); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_NEAR(last_sent_gesture_.data.scroll_update.delta_x, 30.0, 5); |
| } |
| |
| // Tests that when a fling is interrupted (e.g. by having reached the end of |
| // the content), a subsequent fling isn't boosted. An example here would be an |
| // infinite scroller that loads more content after hitting the scroll extent. |
| TEST_P(FlingControllerTest, InterruptedFlingIsntBoosted) { |
| double time_to_advance_ms = 8.0; |
| |
| // Start an ordinary fling. |
| { |
| AdvanceTime(time_to_advance_ms); |
| SimulateScrollBegin(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(10, 0)); |
| SimulateScrollUpdate(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(10, 0)); |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0), |
| /*wait_before_processing=*/false); |
| ASSERT_TRUE(FlingInProgress()); |
| |
| if (NeedsBeginFrameForFlingProgress()) |
| ProgressFling(NowTicks()); |
| } |
| |
| // Stop the fling. This simulates hitting a scroll extent. |
| { |
| ASSERT_EQ(fling_controller_->CurrentFlingVelocity().x(), 1000); |
| fling_controller_->StopFling(); |
| } |
| |
| // Now perform a second fling (e.g. after an infinite scroller loads more |
| // content). Ensure it isn't boosted since the previous fling was |
| // interrupted. |
| { |
| AdvanceTime(time_to_advance_ms); |
| SimulateScrollBegin(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(10, 0)); |
| SimulateScrollUpdate(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(10, 0)); |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0), |
| /*wait_before_processing=*/false); |
| |
| if (NeedsBeginFrameForFlingProgress()) |
| ProgressFling(NowTicks()); |
| |
| EXPECT_EQ(fling_controller_->CurrentFlingVelocity().x(), 1000) |
| << "Fling was boosted but should not have been."; |
| } |
| } |
| |
| TEST_P(FlingControllerTest, ControllerHandlesTouchscreenGestureFling) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| // The fling progress will generate and send GSU events with inertial state. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // Cancellation should send a GSE. |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_EQ(WebInputEvent::Type::kGestureScrollEnd, |
| last_sent_gesture_.GetType()); |
| } |
| |
| TEST_P(FlingControllerTest, ControllerSendsWheelEndWhenTouchpadFlingIsOver) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(100, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| // Processing GFS will send the first fling progress event if the time delta |
| // between the timestamp of the GFS and the time that ProcessGestureFlingStart |
| // is called is large enough. |
| bool process_GFS_sent_first_event = first_wheel_event_sent_; |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| if (!process_GFS_sent_first_event) { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseBegan, last_sent_wheel_.momentum_phase); |
| } else { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, |
| last_sent_wheel_.momentum_phase); |
| } |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| while (FlingInProgress()) { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, |
| last_sent_wheel_.momentum_phase); |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| } |
| |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseEnded, last_sent_wheel_.momentum_phase); |
| EXPECT_EQ(0.f, last_sent_wheel_.delta_x); |
| EXPECT_EQ(0.f, last_sent_wheel_.delta_y); |
| } |
| |
| TEST_P(FlingControllerTest, ControllerSendsGSEWhenTouchscreenFlingIsOver) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(100, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| while (FlingInProgress()) { |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| } |
| |
| EXPECT_EQ(WebInputEvent::Type::kGestureScrollEnd, |
| last_sent_gesture_.GetType()); |
| } |
| |
| TEST_P(FlingControllerTest, EarlyTouchpadFlingCancelationOnFlingStop) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| // Processing GFS will send the first fling progress event if the time delta |
| // between the timestamp of the GFS and the time that ProcessGestureFlingStart |
| // is called is large enough. |
| bool process_GFS_sent_first_event = first_wheel_event_sent_; |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| if (!process_GFS_sent_first_event) { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseBegan, last_sent_wheel_.momentum_phase); |
| } else { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, |
| last_sent_wheel_.momentum_phase); |
| } |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| |
| fling_controller_->StopFling(); |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseEnded, last_sent_wheel_.momentum_phase); |
| EXPECT_EQ(0.f, last_sent_wheel_.delta_x); |
| EXPECT_EQ(0.f, last_sent_wheel_.delta_y); |
| } |
| |
| TEST_P(FlingControllerTest, EarlyTouchscreenFlingCancelationOnFlingStop) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| // progress fling must send GSU events. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| |
| fling_controller_->StopFling(); |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_EQ(WebInputEvent::Type::kGestureScrollEnd, |
| last_sent_gesture_.GetType()); |
| } |
| |
| TEST_P(FlingControllerTest, GestureFlingCancelOutsideFling) { |
| // FlingCancel without a FlingStart doesn't cause issues, doesn't send any |
| // events. |
| { |
| int current_sent_scroll_gesture_count = sent_scroll_gesture_count_; |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_EQ(current_sent_scroll_gesture_count, sent_scroll_gesture_count_); |
| } |
| |
| // Do a fling and cancel it. Make sure another cancel is also a no-op. |
| { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| int current_sent_scroll_gesture_count = sent_scroll_gesture_count_; |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| EXPECT_EQ(current_sent_scroll_gesture_count, sent_scroll_gesture_count_); |
| } |
| } |
| |
| TEST_P(FlingControllerTest, GestureFlingNotCancelledBySmallTimeDelta) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0), false); |
| EXPECT_TRUE(FlingInProgress()); |
| int current_sent_scroll_gesture_count = sent_scroll_gesture_count_; |
| |
| // If we the first progress tick happens too close to the fling_start time, |
| // the controller won't send any GSU events, but the fling is still active. |
| ProgressFling(NowTicks()); |
| EXPECT_EQ(current_sent_scroll_gesture_count, sent_scroll_gesture_count_); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| // The rest of the progress flings must advance the fling normally. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| EXPECT_EQ(blink::WebGestureDevice::kTouchscreen, |
| last_sent_gesture_.SourceDevice()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| } |
| |
| TEST_P(FlingControllerTest, GestureFlingWithNegativeTimeDelta) { |
| base::TimeTicks initial_time = NowTicks(); |
| AdvanceTime(); |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| int current_sent_scroll_gesture_count = sent_scroll_gesture_count_; |
| |
| // If we get a negative time delta, that is, the Progress tick time happens |
| // before the fling's start time then we should *not* try progressing the |
| // fling. |
| ProgressFling(initial_time); |
| EXPECT_EQ(current_sent_scroll_gesture_count, sent_scroll_gesture_count_); |
| |
| // The rest of the progress flings must advance the fling normally. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| EXPECT_EQ(blink::WebGestureDevice::kTouchscreen, |
| last_sent_gesture_.SourceDevice()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| } |
| |
| // Regression test for https://crbug.com/924279 |
| TEST_P(FlingControllerTest, TouchpadFlingWithOldEvent) { |
| // Only the code path that uses compositor animation observers is affected. |
| if (NeedsBeginFrameForFlingProgress()) |
| return; |
| |
| // Create a fling start event. |
| base::TimeTicks event_time = NowTicks(); |
| WebGestureEvent fling_start(WebInputEvent::Type::kGestureFlingStart, 0, |
| event_time, blink::WebGestureDevice::kTouchpad); |
| fling_start.data.fling_start.velocity_x = 0.f; |
| fling_start.data.fling_start.velocity_y = -1000.f; |
| GestureEventWithLatencyInfo fling_start_with_latency(fling_start); |
| |
| // Move time forward. Assume a frame occurs here. |
| AdvanceTime(1.f); |
| base::TimeTicks last_frame_time = NowTicks(); |
| |
| // Start the fling animation later, as if there was a delay in event dispatch. |
| AdvanceTime(1.f); |
| fling_controller_->ProcessGestureFlingStart(fling_start_with_latency); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| // Initial scroll was sent. |
| EXPECT_EQ(1, wheel_event_count_); |
| wheel_event_count_ = 0; |
| |
| // Move time forward a little. |
| AdvanceTime(1.f); |
| |
| // Simulate the compositor animation observer calling ProgressFling with the |
| // last frame time. That frame time is after the event time, but before the |
| // animation start time. |
| ProgressFling(last_frame_time); |
| |
| // No scrolls were sent. |
| EXPECT_EQ(0, wheel_event_count_); |
| |
| // Additional ProgressFling calls generate scroll events as normal. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| EXPECT_GT(wheel_event_count_, 0); |
| } |
| |
| TEST_P(FlingControllerTest, ControllerBoostsTouchpadFling) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| // Processing GFS will send the first fling progress event if the time delta |
| // between the timestamp of the GFS and the time that ProcessGestureFlingStart |
| // is called is large enough. |
| bool process_GFS_sent_first_event = first_wheel_event_sent_; |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| if (!process_GFS_sent_first_event) { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseBegan, last_sent_wheel_.momentum_phase); |
| } else { |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, |
| last_sent_wheel_.momentum_phase); |
| } |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| |
| // The rest of the wheel events must have momentum_phase == KPhaseChanged. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| EXPECT_EQ(WebMouseWheelEvent::kPhaseChanged, last_sent_wheel_.momentum_phase); |
| EXPECT_GT(last_sent_wheel_.delta_x, 0.f); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchpad); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // The second GFS will boost the current active fling. |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| } |
| |
| TEST_P(FlingControllerTest, ControllerBoostsTouchscreenFling) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| // Fling progress must send GSU events. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // The second GFS can be boosted so it should boost the just deactivated |
| // fling. |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| EXPECT_GT(fling_controller_->CurrentFlingVelocity().x(), 1000); |
| } |
| |
| // Ensure that once a fling finishes, the next fling doesn't get boosted. |
| TEST_P(FlingControllerTest, ControllerDoesntBoostFinishedFling) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| |
| // Fast forward so that the fling ends. |
| double time_to_advance_ms = 1000.0; |
| AdvanceTime(time_to_advance_ms); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollEnd, |
| last_sent_gesture_.GetType()) |
| << "Unexpected Last Sent Gesture: " |
| << WebInputEvent::GetName(last_sent_gesture_.GetType()); |
| EXPECT_EQ(fling_controller_->CurrentFlingVelocity().x(), 0); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // Now send a new fling that would have been boosted had it occurred during |
| // the previous fling. Ensure it isn't boosted. |
| AdvanceTime(); |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0), /*wait_before_processing=*/false); |
| EXPECT_TRUE(FlingInProgress()); |
| EXPECT_EQ(fling_controller_->CurrentFlingVelocity().x(), 1000); |
| } |
| |
| TEST_P(FlingControllerTest, ControllerNotifiesTheClientAfterFlingStart) { |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| EXPECT_FALSE(FlingInProgress()); |
| EXPECT_TRUE(notified_client_after_fling_stop_); |
| } |
| |
| TEST_P(FlingControllerTest, MiddleClickAutoScrollFling) { |
| SimulateFlingStart(blink::WebGestureDevice::kSyntheticAutoscroll, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| |
| // Now send a new fling with different velocity and without sending a fling |
| // cancel event, the new fling should always replace the old one even when |
| // they are in the same direction. |
| SimulateFlingStart(blink::WebGestureDevice::kSyntheticAutoscroll, |
| gfx::Vector2dF(2000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| EXPECT_EQ(fling_controller_->CurrentFlingVelocity().x(), 2000); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kSyntheticAutoscroll); |
| EXPECT_FALSE(FlingInProgress()); |
| } |
| |
| // Ensure that the fling controller does not start a fling if the last touchpad |
| // wheel event was consumed. |
| TEST_P(FlingControllerTest, NoFlingStartAfterWheelEventConsumed) { |
| // First ensure that a fling can start after a not consumed wheel event. |
| fling_controller_->OnWheelEventAck( |
| MouseWheelEventWithLatencyInfo(), |
| blink::mojom::InputEventResultSource::kCompositorThread, |
| blink::mojom::InputEventResultState::kNotConsumed); |
| |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(1000, 0)); |
| ASSERT_TRUE(FlingInProgress()); |
| |
| // Cancel the first fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchpad); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // Now test that a consumed touchpad wheel event results in no fling. |
| fling_controller_->OnWheelEventAck( |
| MouseWheelEventWithLatencyInfo(), |
| blink::mojom::InputEventResultSource::kCompositorThread, |
| blink::mojom::InputEventResultState::kConsumed); |
| |
| SimulateFlingStart(blink::WebGestureDevice::kTouchpad, |
| gfx::Vector2dF(1000, 0)); |
| EXPECT_FALSE(FlingInProgress()); |
| } |
| |
| class FlingControllerWithPhysicsBasedFlingTest : public FlingControllerTest { |
| public: |
| // testing::Test |
| FlingControllerWithPhysicsBasedFlingTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kExperimentalFlingAnimation); |
| } |
| |
| FlingControllerWithPhysicsBasedFlingTest( |
| const FlingControllerWithPhysicsBasedFlingTest&) = delete; |
| FlingControllerWithPhysicsBasedFlingTest& operator=( |
| const FlingControllerWithPhysicsBasedFlingTest&) = delete; |
| |
| ~FlingControllerWithPhysicsBasedFlingTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| FlingControllerWithPhysicsBasedFlingTest, |
| testing::Bool()); |
| |
| // Ensure the bounding distance for boosted physics based flings is increased |
| // by a factor of the boost_multiplier and default multiplier |
| TEST_P(FlingControllerWithPhysicsBasedFlingTest, |
| ControllerBoostsTouchscreenFling) { |
| // We use a velocity of 4500 in this test because it yields a scroll delta |
| // that is greater than viewport * boost_multiplier * kDefaultBoundsMultiplier |
| |
| // Android, Chromecast and iOS use Mobile fling curve so they are ignored |
| // for this test |
| bool use_mobile_fling_curve = false; |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CASTOS) || BUILDFLAG(IS_IOS) |
| use_mobile_fling_curve = true; |
| #endif |
| if (use_mobile_fling_curve) |
| return; |
| |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(4500, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| // Fling progress must send GSU events. |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollUpdate, |
| last_sent_gesture_.GetType()); |
| EXPECT_EQ(WebGestureEvent::InertialPhaseState::kMomentum, |
| last_sent_gesture_.data.scroll_update.inertial_phase); |
| EXPECT_GT(last_sent_gesture_.data.scroll_update.delta_x, 0.f); |
| |
| // Now cancel the fling. |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // The second GFS can be boosted so it should boost the just deactivated |
| // fling. To test that the correct bounds scale is used, the scroll delta |
| // is accumulated after each frame. |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(4500, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| if (NeedsBeginFrameForFlingProgress()) |
| ProgressFling(NowTicks()); |
| |
| float total_scroll_delta = CompleteFlingAndAccumulateScrollDelta(); |
| |
| // We expect the scroll delta to be the viewport * [boost_multiplier = 2] * |
| // multiplier |
| float expected_delta = |
| 2 * PhysicsBasedFlingCurve::default_bounds_multiplier_for_testing() * |
| GetRootWidgetViewportSize().width(); |
| EXPECT_EQ(ceilf(total_scroll_delta), roundf(expected_delta)); |
| } |
| |
| // Ensure that once a fling finishes, the next fling has a boost_multiplier of 1 |
| TEST_P(FlingControllerWithPhysicsBasedFlingTest, |
| ControllerDoesntBoostFinishedFling) { |
| // Android, Chromecast and iOS use Mobile fling curve so they are ignored |
| // for this test |
| bool use_mobile_fling_curve = false; |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CASTOS) || BUILDFLAG(IS_IOS) |
| use_mobile_fling_curve = true; |
| #endif |
| if (use_mobile_fling_curve) |
| return; |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(1000, 0), /*wait_before_processing=*/true); |
| EXPECT_TRUE(FlingInProgress()); |
| AdvanceTime(); |
| ProgressFling(NowTicks()); |
| |
| // Fast forward so that the fling ends. |
| double time_to_advance_ms = 1000.0; |
| AdvanceTime(time_to_advance_ms); |
| ProgressFling(NowTicks()); |
| ASSERT_EQ(WebInputEvent::Type::kGestureScrollEnd, |
| last_sent_gesture_.GetType()) |
| << "Unexpected Last Sent Gesture: " |
| << WebInputEvent::GetName(last_sent_gesture_.GetType()); |
| EXPECT_EQ(fling_controller_->CurrentFlingVelocity().x(), 0); |
| EXPECT_FALSE(FlingInProgress()); |
| |
| // Now send a new fling, ensure boost_multiplier is 1 |
| AdvanceTime(); |
| SimulateFlingCancel(blink::WebGestureDevice::kTouchscreen); |
| |
| SimulateFlingStart(blink::WebGestureDevice::kTouchscreen, |
| gfx::Vector2dF(10000, 0)); |
| EXPECT_TRUE(FlingInProgress()); |
| if (NeedsBeginFrameForFlingProgress()) |
| ProgressFling(NowTicks()); |
| |
| float total_scroll_delta = CompleteFlingAndAccumulateScrollDelta(); |
| |
| // We expect the scroll delta to be the viewport * [boost_multiplier = 1] * |
| // multiplier |
| float expected_delta = |
| PhysicsBasedFlingCurve::default_bounds_multiplier_for_testing() * |
| GetRootWidgetViewportSize().width(); |
| EXPECT_EQ(ceilf(total_scroll_delta), roundf(expected_delta)); |
| } |
| |
| } // namespace input |