blob: 8cfee5100d3351b4e482d62143e98611932b49d3 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/scroll/scroll_animator_base.h"
#include "third_party/blink/renderer/core/scroll/scrollbar_test_suite.h"
#include "third_party/blink/renderer/core/scroll/scrollbar_theme.h"
#include "third_party/blink/renderer/core/scroll/scrollbar_theme_overlay_mock.h"
#include "third_party/blink/renderer/core/testing/scoped_mock_overlay_scrollbars.h"
#include "third_party/blink/renderer/platform/graphics/color.h"
#include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support_with_mock_scheduler.h"
namespace blink {
namespace {
using testing::_;
using testing::Mock;
using testing::Return;
class MockAnimatingScrollableArea : public MockScrollableArea {
public:
static MockAnimatingScrollableArea* Create() {
return MakeGarbageCollected<MockAnimatingScrollableArea>();
}
static MockAnimatingScrollableArea* Create(
const ScrollOffset& maximum_scroll_offset) {
MockAnimatingScrollableArea* mock = Create();
mock->SetMaximumScrollOffset(maximum_scroll_offset);
return mock;
}
Scrollbar* HorizontalScrollbar() const override { return nullptr; }
Scrollbar* VerticalScrollbar() const override { return nullptr; }
MOCK_CONST_METHOD0(ScrollAnimatorEnabled, bool());
MOCK_METHOD0(ScheduleAnimation, bool());
};
class ScrollbarThemeWithMockInvalidation : public ScrollbarThemeOverlayMock {
public:
MOCK_CONST_METHOD0(ShouldRepaintAllPartsOnInvalidation, bool());
MOCK_CONST_METHOD3(PartsToInvalidateOnThumbPositionChange,
ScrollbarPart(const Scrollbar&, float, float));
};
} // namespace
class ScrollableAreaTest : public testing::Test {};
TEST_F(ScrollableAreaTest, ScrollAnimatorCurrentPositionShouldBeSync) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
scrollable_area->SetScrollOffset(ScrollOffset(0, 10000),
mojom::blink::ScrollType::kCompositor);
EXPECT_EQ(100.0,
scrollable_area->GetScrollAnimator().CurrentOffset().Height());
}
TEST_F(ScrollableAreaTest, ScrollbarTrackAndThumbRepaint) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
ScrollbarThemeWithMockInvalidation theme;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
Scrollbar* scrollbar = Scrollbar::CreateForTesting(
scrollable_area, kHorizontalScrollbar, &theme);
EXPECT_CALL(theme, ShouldRepaintAllPartsOnInvalidation())
.WillRepeatedly(Return(true));
EXPECT_TRUE(scrollbar->TrackNeedsRepaint());
EXPECT_TRUE(scrollbar->ThumbNeedsRepaint());
scrollbar->SetNeedsPaintInvalidation(kNoPart);
EXPECT_TRUE(scrollbar->TrackNeedsRepaint());
EXPECT_TRUE(scrollbar->ThumbNeedsRepaint());
scrollbar->ClearTrackNeedsRepaint();
scrollbar->ClearThumbNeedsRepaint();
EXPECT_FALSE(scrollbar->TrackNeedsRepaint());
EXPECT_FALSE(scrollbar->ThumbNeedsRepaint());
scrollbar->SetNeedsPaintInvalidation(kThumbPart);
EXPECT_TRUE(scrollbar->TrackNeedsRepaint());
EXPECT_TRUE(scrollbar->ThumbNeedsRepaint());
// When not all parts are repainted on invalidation,
// setNeedsPaintInvalidation sets repaint bits only on the requested parts.
EXPECT_CALL(theme, ShouldRepaintAllPartsOnInvalidation())
.WillRepeatedly(Return(false));
scrollbar->ClearTrackNeedsRepaint();
scrollbar->ClearThumbNeedsRepaint();
EXPECT_FALSE(scrollbar->TrackNeedsRepaint());
EXPECT_FALSE(scrollbar->ThumbNeedsRepaint());
scrollbar->SetNeedsPaintInvalidation(kThumbPart);
EXPECT_FALSE(scrollbar->TrackNeedsRepaint());
EXPECT_TRUE(scrollbar->ThumbNeedsRepaint());
// Forced GC in order to finalize objects depending on the mock object.
ThreadState::Current()->CollectAllGarbageForTesting();
}
TEST_F(ScrollableAreaTest, ScrollbarLayerInvalidation) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
ScopedMockOverlayScrollbars mock_overlay_scrollbars;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
scoped_refptr<cc::Layer> layer = cc::Layer::Create();
layer->SetIsDrawable(true);
layer->SetBounds(gfx::Size(111, 222));
EXPECT_CALL(*scrollable_area, LayerForHorizontalScrollbar())
.WillRepeatedly(Return(layer.get()));
auto* scrollbar = MakeGarbageCollected<Scrollbar>(
scrollable_area, kHorizontalScrollbar, nullptr, nullptr);
EXPECT_TRUE(layer->update_rect().IsEmpty());
scrollbar->SetNeedsPaintInvalidation(kNoPart);
EXPECT_FALSE(layer->update_rect().IsEmpty());
// Forced GC in order to finalize objects depending on the mock object.
ThreadState::Current()->CollectAllGarbageForTesting();
}
TEST_F(ScrollableAreaTest, InvalidatesNonCompositedScrollbarsWhenThumbMoves) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
ScrollbarThemeWithMockInvalidation theme;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(100, 100));
Scrollbar* horizontal_scrollbar = Scrollbar::CreateForTesting(
scrollable_area, kHorizontalScrollbar, &theme);
Scrollbar* vertical_scrollbar =
Scrollbar::CreateForTesting(scrollable_area, kVerticalScrollbar, &theme);
EXPECT_CALL(*scrollable_area, HorizontalScrollbar())
.WillRepeatedly(Return(horizontal_scrollbar));
EXPECT_CALL(*scrollable_area, VerticalScrollbar())
.WillRepeatedly(Return(vertical_scrollbar));
// Regardless of whether the theme invalidates any parts, non-composited
// scrollbars have to be repainted if the thumb moves.
EXPECT_CALL(*scrollable_area, LayerForHorizontalScrollbar())
.WillRepeatedly(Return(nullptr));
EXPECT_CALL(*scrollable_area, LayerForVerticalScrollbar())
.WillRepeatedly(Return(nullptr));
ASSERT_FALSE(scrollable_area->HasLayerForVerticalScrollbar());
ASSERT_FALSE(scrollable_area->HasLayerForHorizontalScrollbar());
EXPECT_CALL(theme, ShouldRepaintAllPartsOnInvalidation())
.WillRepeatedly(Return(false));
EXPECT_CALL(theme, PartsToInvalidateOnThumbPositionChange(_, _, _))
.WillRepeatedly(Return(kNoPart));
// A scroll in each direction should only invalidate one scrollbar.
scrollable_area->SetScrollOffset(ScrollOffset(0, 50),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_FALSE(scrollable_area->HorizontalScrollbarNeedsPaintInvalidation());
EXPECT_TRUE(scrollable_area->VerticalScrollbarNeedsPaintInvalidation());
scrollable_area->ClearNeedsPaintInvalidationForScrollControls();
scrollable_area->SetScrollOffset(ScrollOffset(50, 50),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_TRUE(scrollable_area->HorizontalScrollbarNeedsPaintInvalidation());
EXPECT_FALSE(scrollable_area->VerticalScrollbarNeedsPaintInvalidation());
scrollable_area->ClearNeedsPaintInvalidationForScrollControls();
// Forced GC in order to finalize objects depending on the mock object.
ThreadState::Current()->CollectAllGarbageForTesting();
}
TEST_F(ScrollableAreaTest, InvalidatesCompositedScrollbarsIfPartsNeedRepaint) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
ScrollbarThemeWithMockInvalidation theme;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(100, 100));
Scrollbar* horizontal_scrollbar = Scrollbar::CreateForTesting(
scrollable_area, kHorizontalScrollbar, &theme);
horizontal_scrollbar->ClearTrackNeedsRepaint();
horizontal_scrollbar->ClearThumbNeedsRepaint();
Scrollbar* vertical_scrollbar =
Scrollbar::CreateForTesting(scrollable_area, kVerticalScrollbar, &theme);
vertical_scrollbar->ClearTrackNeedsRepaint();
vertical_scrollbar->ClearThumbNeedsRepaint();
EXPECT_CALL(*scrollable_area, HorizontalScrollbar())
.WillRepeatedly(Return(horizontal_scrollbar));
EXPECT_CALL(*scrollable_area, VerticalScrollbar())
.WillRepeatedly(Return(vertical_scrollbar));
// Composited scrollbars only need repainting when parts become invalid
// (e.g. if the track changes appearance when the thumb reaches the end).
scoped_refptr<cc::Layer> layer_for_horizontal_scrollbar = cc::Layer::Create();
layer_for_horizontal_scrollbar->SetIsDrawable(true);
layer_for_horizontal_scrollbar->SetBounds(gfx::Size(10, 10));
scoped_refptr<cc::Layer> layer_for_vertical_scrollbar = cc::Layer::Create();
layer_for_vertical_scrollbar->SetIsDrawable(true);
layer_for_vertical_scrollbar->SetBounds(gfx::Size(10, 10));
EXPECT_CALL(*scrollable_area, LayerForHorizontalScrollbar())
.WillRepeatedly(Return(layer_for_horizontal_scrollbar.get()));
EXPECT_CALL(*scrollable_area, LayerForVerticalScrollbar())
.WillRepeatedly(Return(layer_for_vertical_scrollbar.get()));
ASSERT_TRUE(scrollable_area->HasLayerForHorizontalScrollbar());
ASSERT_TRUE(scrollable_area->HasLayerForVerticalScrollbar());
EXPECT_CALL(theme, ShouldRepaintAllPartsOnInvalidation())
.WillRepeatedly(Return(false));
EXPECT_TRUE(layer_for_horizontal_scrollbar->update_rect().IsEmpty());
EXPECT_TRUE(layer_for_vertical_scrollbar->update_rect().IsEmpty());
// First, we'll scroll horizontally, and the theme will require repainting
// the back button (i.e. the track).
EXPECT_CALL(theme, PartsToInvalidateOnThumbPositionChange(_, _, _))
.WillOnce(Return(kBackButtonStartPart));
scrollable_area->SetScrollOffset(ScrollOffset(50, 0),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_FALSE(layer_for_horizontal_scrollbar->update_rect().IsEmpty());
EXPECT_TRUE(layer_for_vertical_scrollbar->update_rect().IsEmpty());
EXPECT_TRUE(horizontal_scrollbar->TrackNeedsRepaint());
EXPECT_FALSE(horizontal_scrollbar->ThumbNeedsRepaint());
layer_for_horizontal_scrollbar->ResetUpdateRectForTesting();
horizontal_scrollbar->ClearTrackNeedsRepaint();
// Next, we'll scroll vertically, but invalidate the thumb.
EXPECT_CALL(theme, PartsToInvalidateOnThumbPositionChange(_, _, _))
.WillOnce(Return(kThumbPart));
scrollable_area->SetScrollOffset(ScrollOffset(50, 50),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_TRUE(layer_for_horizontal_scrollbar->update_rect().IsEmpty());
EXPECT_FALSE(layer_for_vertical_scrollbar->update_rect().IsEmpty());
EXPECT_FALSE(vertical_scrollbar->TrackNeedsRepaint());
EXPECT_TRUE(vertical_scrollbar->ThumbNeedsRepaint());
layer_for_vertical_scrollbar->ResetUpdateRectForTesting();
vertical_scrollbar->ClearThumbNeedsRepaint();
// Next we'll scroll in both, but the thumb position moving requires no
// invalidations. Nonetheless the GraphicsLayer should be invalidated,
// because we still need to update the underlying layer (though no
// rasterization will be required).
EXPECT_CALL(theme, PartsToInvalidateOnThumbPositionChange(_, _, _))
.Times(2)
.WillRepeatedly(Return(kNoPart));
scrollable_area->SetScrollOffset(ScrollOffset(70, 70),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_FALSE(layer_for_horizontal_scrollbar->update_rect().IsEmpty());
EXPECT_FALSE(layer_for_vertical_scrollbar->update_rect().IsEmpty());
EXPECT_FALSE(horizontal_scrollbar->TrackNeedsRepaint());
EXPECT_FALSE(horizontal_scrollbar->ThumbNeedsRepaint());
EXPECT_FALSE(vertical_scrollbar->TrackNeedsRepaint());
EXPECT_FALSE(vertical_scrollbar->ThumbNeedsRepaint());
// Forced GC in order to finalize objects depending on the mock object.
ThreadState::Current()->CollectAllGarbageForTesting();
}
TEST_F(ScrollableAreaTest, RecalculatesScrollbarOverlayIfBackgroundChanges) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
EXPECT_EQ(kScrollbarOverlayColorThemeDark,
scrollable_area->GetScrollbarOverlayColorTheme());
scrollable_area->RecalculateScrollbarOverlayColorTheme(Color(34, 85, 51));
EXPECT_EQ(kScrollbarOverlayColorThemeLight,
scrollable_area->GetScrollbarOverlayColorTheme());
scrollable_area->RecalculateScrollbarOverlayColorTheme(Color(236, 143, 185));
EXPECT_EQ(kScrollbarOverlayColorThemeDark,
scrollable_area->GetScrollbarOverlayColorTheme());
}
TEST_F(ScrollableAreaTest, ScrollableAreaDidScroll) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(100, 100));
scrollable_area->DidScroll(FloatPoint(40, 51));
EXPECT_EQ(40, scrollable_area->ScrollOffsetInt().Width());
EXPECT_EQ(51, scrollable_area->ScrollOffsetInt().Height());
}
TEST_F(ScrollableAreaTest, ProgrammaticScrollRespectAnimatorEnabled) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockAnimatingScrollableArea* scrollable_area =
MockAnimatingScrollableArea::Create(ScrollOffset(0, 100));
// Disable animations. Make sure an explicitly smooth programmatic scroll is
// instantly scrolled.
{
EXPECT_CALL(*scrollable_area, ScrollAnimatorEnabled())
.WillRepeatedly(Return(false));
EXPECT_CALL(*scrollable_area, ScheduleAnimation()).Times(0);
scrollable_area->SetScrollOffset(ScrollOffset(0, 100),
mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kSmooth);
EXPECT_EQ(100, scrollable_area->GetScrollOffset().Height());
}
Mock::VerifyAndClearExpectations(scrollable_area);
// Enable animations. A smooth programmatic scroll should now schedule an
// animation rather than immediately mutating the offset.
{
EXPECT_CALL(*scrollable_area, ScrollAnimatorEnabled())
.WillRepeatedly(Return(true));
EXPECT_CALL(*scrollable_area, ScheduleAnimation()).WillOnce(Return(true));
scrollable_area->SetScrollOffset(ScrollOffset(0, 50),
mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kSmooth);
// Offset is unchanged.
EXPECT_EQ(100, scrollable_area->GetScrollOffset().Height());
}
}
// Scrollbars in popups shouldn't fade out since they aren't composited and thus
// they don't appear on hover so users without a wheel can't scroll if they fade
// out.
TEST_F(ScrollableAreaTest, PopupOverlayScrollbarShouldNotFadeOut) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
ScopedMockOverlayScrollbars mock_overlay_scrollbars;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
scrollable_area->SetIsPopup();
ScrollbarThemeOverlayMock& theme =
(ScrollbarThemeOverlayMock&)scrollable_area->GetPageScrollbarTheme();
theme.SetOverlayScrollbarFadeOutDelay(base::TimeDelta::FromSeconds(1));
Scrollbar* scrollbar = Scrollbar::CreateForTesting(
scrollable_area, kHorizontalScrollbar, &theme);
DCHECK(scrollbar->IsOverlayScrollbar());
DCHECK(scrollbar->Enabled());
scrollable_area->ShowNonMacOverlayScrollbars();
// No fade out animation should be posted.
EXPECT_FALSE(scrollable_area->fade_overlay_scrollbars_timer_);
// Forced GC in order to finalize objects depending on the mock object.
ThreadState::Current()->CollectAllGarbageForTesting();
}
TEST_F(ScrollableAreaTest, ScrollAnimatorCallbackFiresOnAnimationCancel) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
EXPECT_CALL(*scrollable_area, ScheduleAnimation())
.WillRepeatedly(Return(true));
bool finished = false;
scrollable_area->SetScrollOffset(
ScrollOffset(0, 10000), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kSmooth,
ScrollableArea::ScrollCallback(
base::BindOnce([](bool* finished) { *finished = true; }, &finished)));
EXPECT_EQ(0.0, scrollable_area->GetScrollAnimator().CurrentOffset().Height());
EXPECT_FALSE(finished);
scrollable_area->CancelProgrammaticScrollAnimation();
EXPECT_EQ(0.0, scrollable_area->GetScrollAnimator().CurrentOffset().Height());
EXPECT_TRUE(finished);
}
TEST_F(ScrollableAreaTest, ScrollAnimatorCallbackFiresOnInstantScroll) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
EXPECT_CALL(*scrollable_area, ScheduleAnimation())
.WillRepeatedly(Return(true));
bool finished = false;
scrollable_area->SetScrollOffset(
ScrollOffset(0, 10000), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kInstant,
ScrollableArea::ScrollCallback(
base::BindOnce([](bool* finished) { *finished = true; }, &finished)));
EXPECT_EQ(100, scrollable_area->GetScrollAnimator().CurrentOffset().Height());
EXPECT_TRUE(finished);
}
TEST_F(ScrollableAreaTest, ScrollAnimatorCallbackFiresOnAnimationFinish) {
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform;
MockScrollableArea* scrollable_area =
MockScrollableArea::Create(ScrollOffset(0, 100));
EXPECT_CALL(*scrollable_area, ScheduleAnimation())
.WillRepeatedly(Return(true));
bool finished = false;
scrollable_area->SetScrollOffset(
ScrollOffset(0, 9), mojom::blink::ScrollType::kProgrammatic,
mojom::blink::ScrollBehavior::kSmooth,
ScrollableArea::ScrollCallback(
base::BindOnce([](bool* finished) { *finished = true; }, &finished)));
EXPECT_EQ(0.0, scrollable_area->GetScrollAnimator().CurrentOffset().Height());
EXPECT_FALSE(finished);
scrollable_area->UpdateCompositorScrollAnimations();
scrollable_area->ServiceScrollAnimations(1);
EXPECT_EQ(0.0, scrollable_area->GetScrollAnimator().CurrentOffset().Height());
EXPECT_FALSE(finished);
scrollable_area->ServiceScrollAnimations(1000000);
EXPECT_EQ(9.0, scrollable_area->GetScrollAnimator().CurrentOffset().Height());
EXPECT_TRUE(finished);
}
} // namespace blink