blob: 995fce1a0219b031b85e344cd8221b6fa5a4ec7d [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cc/input/scroll_timing_controller.h"
#include "cc/input/scroll_timing_info.h"
#include "cc/paint/element_id.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/types/scroll_input_type.h"
namespace cc {
namespace {
constexpr ElementId kScrollerId(7);
class ScrollTimingControllerTest : public testing::Test {
protected:
ScrollTimingController controller_;
};
// A touchscreen gesture with a non-null hardware timestamp that latches a
// scroller produces exactly one ScrollTimingInfo with the latched element
// id, the original input type, the hardware start_time, and a non-decreasing
// end_time.
TEST_F(ScrollTimingControllerTest, TouchScrollEmitsRecord) {
const base::TimeTicks event_timestamp =
base::TimeTicks::Now() - base::Milliseconds(7);
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen,
event_timestamp);
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kTouchscreen);
const std::vector<ScrollTimingInfo> infos =
controller_.TakeCompletedScrollTimingInfos();
ASSERT_EQ(1u, infos.size());
EXPECT_EQ(kScrollerId, infos.front().element_id);
EXPECT_EQ(ui::ScrollInputType::kTouchscreen, infos.front().input_type);
EXPECT_EQ(event_timestamp, infos.front().start_time);
ASSERT_TRUE(infos.front().end_time.has_value());
EXPECT_GE(*infos.front().end_time, infos.front().start_time);
}
// Wheel scrolls are also within scope of the API.
TEST_F(ScrollTimingControllerTest, WheelScrollEmitsRecord) {
controller_.DidScrollBegin(ui::ScrollInputType::kWheel,
base::TimeTicks::Now());
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kWheel);
const std::vector<ScrollTimingInfo> infos =
controller_.TakeCompletedScrollTimingInfos();
ASSERT_EQ(1u, infos.size());
EXPECT_EQ(ui::ScrollInputType::kWheel, infos.front().input_type);
}
// When DidScrollBegin is never called (the InputHandlerProxy's signal that
// the runtime feature is off), updates and ends are no-ops and no record is
// produced.
TEST_F(ScrollTimingControllerTest, NoBeginNoRecord) {
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kWheel);
EXPECT_TRUE(controller_.TakeCompletedScrollTimingInfos().empty());
}
// A gesture that begins but never gets an effective update (no latched
// element id) is dropped on end.
TEST_F(ScrollTimingControllerTest, NoUpdateNoRecord) {
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen,
base::TimeTicks::Now());
controller_.DidScrollEnd(ui::ScrollInputType::kTouchscreen);
EXPECT_TRUE(controller_.TakeCompletedScrollTimingInfos().empty());
}
// Input types outside the API's scope (e.g. scrollbar, autoscroll) are
// silently ignored at DidScrollBegin so no tracking starts and no record
// is produced.
TEST_F(ScrollTimingControllerTest, ScrollbarInputTypeNotTracked) {
controller_.DidScrollBegin(ui::ScrollInputType::kScrollbar,
base::TimeTicks::Now());
EXPECT_FALSE(controller_.ActiveScrollStartForTesting().has_value());
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kScrollbar);
EXPECT_TRUE(controller_.TakeCompletedScrollTimingInfos().empty());
}
// A wrong-device GestureScrollEnd that races a successful gesture must not
// finalize the in-flight record, and also drops the (now-suspect) in-flight
// tracking so a stale entry can't be emitted later.
TEST_F(ScrollTimingControllerTest, InputTypeMismatchOnEndDropsEnd) {
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen,
base::TimeTicks::Now());
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kWheel);
EXPECT_TRUE(controller_.TakeCompletedScrollTimingInfos().empty());
EXPECT_FALSE(controller_.ActiveScrollStartForTesting().has_value());
}
// A null hardware timestamp at DidScrollBegin (e.g. synthetic/legacy callers)
// is silently dropped so no entry with an incomparable startTime is emitted.
TEST_F(ScrollTimingControllerTest, NullTimestampNotTracked) {
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen,
base::TimeTicks());
EXPECT_FALSE(controller_.ActiveScrollStartForTesting().has_value());
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kTouchscreen);
EXPECT_TRUE(controller_.TakeCompletedScrollTimingInfos().empty());
}
// A second DidScrollBegin without an intervening DidScrollEnd overwrites the
// in-flight gesture. The resulting record reflects the second begin's
// start_time and the element latched after the second begin.
TEST_F(ScrollTimingControllerTest, SecondBeginOverwritesInFlightGesture) {
constexpr ElementId kFirstScrollerId(11);
constexpr ElementId kSecondScrollerId(22);
const base::TimeTicks first_begin =
base::TimeTicks::Now() - base::Milliseconds(20);
const base::TimeTicks second_begin =
base::TimeTicks::Now() - base::Milliseconds(5);
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen, first_begin);
controller_.DidScrollUpdate(kFirstScrollerId);
// No DidScrollEnd before the next begin.
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen, second_begin);
controller_.DidScrollUpdate(kSecondScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kTouchscreen);
const std::vector<ScrollTimingInfo> infos =
controller_.TakeCompletedScrollTimingInfos();
ASSERT_EQ(1u, infos.size());
EXPECT_EQ(second_begin, infos.front().start_time);
EXPECT_EQ(kSecondScrollerId, infos.front().element_id);
}
// TakeCompletedScrollTimingInfos drains the batch: a subsequent call before
// any further activity returns an empty vector.
TEST_F(ScrollTimingControllerTest, TakeDrainsCompletedRecords) {
controller_.DidScrollBegin(ui::ScrollInputType::kTouchscreen,
base::TimeTicks::Now());
controller_.DidScrollUpdate(kScrollerId);
controller_.DidScrollEnd(ui::ScrollInputType::kTouchscreen);
EXPECT_EQ(1u, controller_.TakeCompletedScrollTimingInfos().size());
EXPECT_TRUE(controller_.TakeCompletedScrollTimingInfos().empty());
}
} // namespace
} // namespace cc