chromium/ui/views/controls/scroll_view_unittest.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/scroll_view.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/gtest_util.h"
#include "base/test/icu_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/controls/scrollbar/scroll_bar_views.h"
#include "ui/views/test/test_views.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view_observer.h"
#include "ui/views/view_test_api.h"
#include "ui/views/view_tracker.h"

#if BUILDFLAG(IS_MAC)
#include "ui/base/test/scoped_preferred_scroller_style_mac.h"
#endif

enum ScrollBarOrientation {};

namespace views {
namespace test {

class ScrollViewTestApi {};

}  // namespace test

namespace {

const int kWidth =;
const int kMinHeight =;
const int kMaxHeight =;

class FixedView : public View {};

BEGIN_METADATA()

class CustomView : public View {};

BEGIN_METADATA()

void CheckScrollbarVisibility(const ScrollView* scroll_view,
                              ScrollBarOrientation orientation,
                              bool should_be_visible) {}

ui::MouseEvent TestLeftMouseAt(const gfx::Point& location, ui::EventType type) {}

// This view has a large width, but the height always matches the parent's
// height. This is similar to a TableView that has many columns showing, but
// very few rows.
class VerticalResizingView : public View {};

BEGIN_METADATA()

// Same as VerticalResizingView, but horizontal instead.
class HorizontalResizingView : public View {};

BEGIN_METADATA()

class TestScrollBarThumb : public BaseScrollBarThumb {};

BEGIN_METADATA()

class TestScrollBar : public ScrollBar {};

BEGIN_METADATA()

}  // namespace

ScrollViewTestApi;

// Simple test harness for testing a ScrollView directly.
class ScrollViewTest : public ViewsTestBase {};

// Test harness that includes a Widget to help test ui::Event handling.
class WidgetScrollViewTest : public test::WidgetTest,
                             public ui::CompositorObserver {};

constexpr int WidgetScrollViewTest::kDefaultHeight;
constexpr int WidgetScrollViewTest::kDefaultWidth;

// A gtest parameter to permute over whether ScrollView uses a left-to-right or
// right-to-left locale, or whether it uses ui::Layers or View bounds offsets to
// position contents (i.e. ::features::kUiCompositorScrollWithLayers).
enum class UiConfig {};

class WidgetScrollViewTestRTLAndLayers
    : public WidgetScrollViewTest,
      public ::testing::WithParamInterface<UiConfig> {};

std::string UiConfigToString(const testing::TestParamInfo<UiConfig>& info) {}

// Verifies the viewport is sized to fit the available space.
TEST_F(ScrollViewTest, ViewportSizedToFit) {}

// Verifies the viewport and content is sized to fit the available space for
// bounded scroll view.
TEST_F(ScrollViewTest, BoundedViewportSizedToFit) {}

// Verifies that the vertical scrollbar does not unnecessarily appear for a
// contents whose height always matches the height of the viewport.
TEST_F(ScrollViewTest, VerticalScrollbarDoesNotAppearUnnecessarily) {}

// Same as above, but setting horizontal scroll bar to hidden.
TEST_F(ScrollViewTest, HorizontalScrollbarDoesNotAppearIfHidden) {}

// Same as above, but setting vertical scrollbar instead.
TEST_F(ScrollViewTest, VerticalScrollbarDoesNotAppearIfHidden) {}

// Same as above, but setting horizontal scroll bar to disabled.
TEST_F(ScrollViewTest, HorizontalScrollbarDoesNotAppearIfDisabled) {}

// Same as above, but setting vertical scrollbar instead.
TEST_F(ScrollViewTest, VerticallScrollbarDoesNotAppearIfDisabled) {}

TEST_F(ScrollViewTest, AccessibleProperties) {}

// Verifies the scrollbars are added as necessary.
// If on Mac, test the non-overlay scrollbars.
TEST_F(ScrollViewTest, ScrollBars) {}

// Tests that after scrolling the child (which was gained the focus) into the
// viewport of the scroll view, the tooltip should be shown up.
TEST_F(WidgetScrollViewTest, ScrollChildToVisibleOnFocusWithTooltip) {}

// Assertions around adding a header.
TEST_F(WidgetScrollViewTest, Header) {}

// Verifies the scrollbars are added as necessary when a header is present.
TEST_F(ScrollViewTest, ScrollBarsWithHeader) {}

// Verifies the header scrolls horizontally with the content.
TEST_F(ScrollViewTest, HeaderScrollsWithContent) {}

// Test that calling ScrollToPosition() also updates the position of the
// corresponding ScrollBar.
TEST_F(ScrollViewTest, ScrollToPositionUpdatesScrollBar) {}

TEST_F(ScrollViewTest, HorizontalScrollBarAccessibleScrollXProperties) {}

TEST_F(ScrollViewTest, VerticalScrollBarAccessibleScrollYProperties) {}

// Test that calling ScrollToPosition() also updates the position of the
// child view even when the horizontal scrollbar is hidden.
TEST_F(ScrollViewTest, ScrollToPositionUpdatesWithHiddenHorizontalScrollBar) {}

// Test that calling ScrollToPosition() also updates the position of the
// child view even when the horizontal scrollbar is hidden.
TEST_F(ScrollViewTest, ScrollToPositionUpdatesWithHiddenVerticalScrollBar) {}

// Verifies ScrollRectToVisible() on the child works.
TEST_F(ScrollViewTest, ScrollRectToVisible) {}

// Verifies ScrollByOffset() method works as expected
TEST_F(ScrollViewTest, ScrollByOffset) {}

// Verifies ScrollRectToVisible() scrolls the view horizontally even if the
// horizontal scrollbar is hidden (but not disabled).
TEST_F(ScrollViewTest, ScrollRectToVisibleWithHiddenHorizontalScrollbar) {}

// Verifies ScrollRectToVisible() scrolls the view vertically even if the
// vertical scrollbar is hidden (but not disabled).
TEST_F(ScrollViewTest, ScrollRectToVisibleWithHiddenVerticalScrollbar) {}

// Verifies ScrollRectToVisible() does not scroll the view horizontally or
// vertically if the scrollbars are disabled.
TEST_F(ScrollViewTest, ScrollRectToVisibleWithDisabledScrollbars) {}

// Verifies that child scrolls into view when it's focused.
TEST_F(ScrollViewTest, ScrollChildToVisibleOnFocus) {}

// Verifies that ScrollView scrolls into view when its contents root is focused.
TEST_F(ScrollViewTest, ScrollViewToVisibleOnContentsRootFocus) {}

// Verifies ClipHeightTo() uses the height of the content when it is between the
// minimum and maximum height values.
TEST_F(ScrollViewTest, ClipHeightToNormalContentHeight) {}

// Verifies ClipHeightTo() uses the minimum height when the content is shorter
// than the minimum height value.
TEST_F(ScrollViewTest, ClipHeightToShortContentHeight) {}

// Verifies ClipHeightTo() uses the maximum height when the content is longer
// thamn the maximum height value.
TEST_F(ScrollViewTest, ClipHeightToTallContentHeight) {}

// Verifies that when ClipHeightTo() produces a scrollbar, it reduces the width
// of the inner content of the ScrollView.
TEST_F(ScrollViewTest, ClipHeightToScrollbarUsesWidth) {}

// Verifies ClipHeightTo() updates the ScrollView's preferred size.
TEST_F(ScrollViewTest, ClipHeightToUpdatesPreferredSize) {}

TEST_F(ScrollViewTest, CornerViewVisibility) {}

// This test needs a widget so that color changes will be reflected.
TEST_F(WidgetScrollViewTest, ChildWithLayerTest) {}

// Validates that if a child of a ScrollView adds a layer, then a layer
// is not added to the ScrollView's viewport.
TEST_F(ScrollViewTest, DontCreateLayerOnViewportIfLayerOnScrollViewCreated) {}

// Validates if the contents_viewport uses correct layer type when adding views
// with different types of layers.
TEST_F(ScrollViewTest, ContentsViewportLayerUsed_ScrollWithLayersDisabled) {}

// Validates the layer of contents_viewport_, when contents_ does not have a
// layer.
TEST_F(
    ScrollViewTest,
    ContentsViewportLayerWhenContentsDoesNotHaveLayer_ScrollWithLayersDisabled) {}

// Validates if scroll_with_layers is enabled, we disallow to change the layer
// of contents_  once the contents of ScrollView are set.
TEST_F(
    ScrollViewTest,
    ContentsLayerCannotBeChangedAfterContentsAreSet_ScrollWithLayersEnabled) {}

// Validates if scroll_with_layers is disabled, we can change the layer of
// contents_ once the contents of ScrollView are set.
TEST_F(ScrollViewTest,
       ContentsLayerCanBeChangedAfterContentsAreSet_ScrollWithLayersDisabled) {}

// Validates if the content of contents_viewport is changed, a correct layer is
// used for contents_viewport.
TEST_F(
    ScrollViewTest,
    ContentsViewportLayerUsedWhenScrollViewContentsAreChanged_ScrollWithLayersDisabled) {}

// Validates correct behavior of layers used for contents_viewport used when
// scroll with layers is enabled.
TEST_F(ScrollViewTest, ContentsViewportLayerUsed_ScrollWithLayersEnabled) {}

// Validates if correct layers are used for contents_viewport used when
// ScrollView enables a NOT_DRAWN layer on contents when scroll with layers in
// enabled.
TEST_F(
    ScrollViewTest,
    ContentsViewportLayerUsedWhenNotDrawnUsedForContents_ScrollWithLayersEnabled) {}

TEST_F(ScrollViewTest,
       ContentsViewportLayerHasRoundedCorners_ScrollWithLayersEnabled) {}

#if BUILDFLAG(IS_MAC)
// Tests the overlay scrollbars on Mac. Ensure that they show up properly and
// do not overlap each other.
TEST_F(ScrollViewTest, CocoaOverlayScrollBars) {
  SetOverlayScrollersEnabled(true);
  View* contents = InstallContents();

  // Size the contents such that vertical scrollbar is needed.
  // Since it is overlaid, the ViewPort size should match the ScrollView.
  contents->SetBounds(0, 0, 50, 400);
  InvalidateAndRunScheduledLayoutOnScrollView();
  EXPECT_EQ(100, contents->parent()->width());
  EXPECT_EQ(100, contents->parent()->height());
  EXPECT_EQ(0, scroll_view_->GetScrollBarLayoutWidth());
  CheckScrollbarVisibility(scroll_view_.get(), VERTICAL, true);
  CheckScrollbarVisibility(scroll_view_.get(), HORIZONTAL, false);

  // Size the contents such that horizontal scrollbar is needed.
  contents->SetBounds(0, 0, 400, 50);
  InvalidateAndRunScheduledLayoutOnScrollView();
  EXPECT_EQ(100, contents->parent()->width());
  EXPECT_EQ(100, contents->parent()->height());
  EXPECT_EQ(0, scroll_view_->GetScrollBarLayoutHeight());
  CheckScrollbarVisibility(scroll_view_.get(), VERTICAL, false);
  CheckScrollbarVisibility(scroll_view_.get(), HORIZONTAL, true);

  // Both horizontal and vertical scrollbars.
  contents->SetBounds(0, 0, 300, 400);
  InvalidateAndRunScheduledLayoutOnScrollView();
  EXPECT_EQ(100, contents->parent()->width());
  EXPECT_EQ(100, contents->parent()->height());
  EXPECT_EQ(0, scroll_view_->GetScrollBarLayoutWidth());
  EXPECT_EQ(0, scroll_view_->GetScrollBarLayoutHeight());
  CheckScrollbarVisibility(scroll_view_.get(), VERTICAL, true);
  CheckScrollbarVisibility(scroll_view_.get(), HORIZONTAL, true);

  // Make sure the horizontal and vertical scrollbars don't overlap each other.
  gfx::Rect vert_bounds = scroll_view_->vertical_scroll_bar()->bounds();
  gfx::Rect horiz_bounds = scroll_view_->horizontal_scroll_bar()->bounds();
  EXPECT_EQ(vert_bounds.x(), horiz_bounds.right());
  EXPECT_EQ(horiz_bounds.y(), vert_bounds.bottom());

  // Switch to the non-overlay style and check that the ViewPort is now sized
  // to be smaller, and ScrollbarWidth and ScrollbarHeight are non-zero.
  SetOverlayScrollersEnabled(false);
  EXPECT_TRUE(ViewTestApi(scroll_view_.get()).needs_layout());
  views::test::RunScheduledLayout(scroll_view_.get());
  EXPECT_EQ(100 - VerticalScrollBarWidth(), contents->parent()->width());
  EXPECT_EQ(100 - HorizontalScrollBarHeight(), contents->parent()->height());
  EXPECT_NE(0, VerticalScrollBarWidth());
  EXPECT_NE(0, HorizontalScrollBarHeight());
}

// Test that overlay scroll bars will only process events when visible.
TEST_F(WidgetScrollViewTest,
       OverlayScrollBarsCannotProcessEventsWhenTransparent) {
  // Allow expectations to distinguish between fade outs and immediate changes.
  ui::ScopedAnimationDurationScaleMode really_animate(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  SetUseOverlayScrollers();

  ScrollView* scroll_view = AddScrollViewWithContentSize(
      gfx::Size(kDefaultWidth * 5, kDefaultHeight * 5));
  ScrollViewTestApi test_api(scroll_view);
  ScrollBar* scroll_bar = test_api.GetScrollBar(HORIZONTAL);

  // Verify scroll bar is unable to process events.
  EXPECT_FALSE(scroll_bar->GetCanProcessEventsWithinSubtree());

  ui::test::EventGenerator generator(
      GetContext(), scroll_view->GetWidget()->GetNativeWindow());

  generator.GenerateTrackpadRest();

  // Since the scroll bar will become visible, it should now be able to process
  // events.
  EXPECT_TRUE(scroll_bar->GetCanProcessEventsWithinSubtree());
}

// Test overlay scrollbar behavior when just resting fingers on the trackpad.
TEST_F(WidgetScrollViewTest, ScrollersOnRest) {
  // Allow expectations to distinguish between fade outs and immediate changes.
  ui::ScopedAnimationDurationScaleMode really_animate(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  const float kMaxOpacity = 0.8f;  // Constant from cocoa_scroll_bar.mm.

  SetUseOverlayScrollers();

  // Set up with both scrollers.
  ScrollView* scroll_view = AddScrollViewWithContentSize(
      gfx::Size(kDefaultWidth * 5, kDefaultHeight * 5));
  ScrollViewTestApi test_api(scroll_view);
  const auto bar = std::to_array<ScrollBar*>(
      {test_api.GetScrollBar(HORIZONTAL), test_api.GetScrollBar(VERTICAL)});
  const auto hide_timer = std::to_array<base::RetainingOneShotTimer*>(
      {test_api.GetScrollBarHideTimer(HORIZONTAL),
       test_api.GetScrollBarHideTimer(VERTICAL)});

  EXPECT_EQ(0, bar[HORIZONTAL]->layer()->opacity());
  EXPECT_EQ(0, bar[VERTICAL]->layer()->opacity());

  ui::test::EventGenerator generator(
      GetContext(), scroll_view->GetWidget()->GetNativeWindow());

  generator.GenerateTrackpadRest();
  // Scrollers should be max opacity without an animation.
  EXPECT_EQ(kMaxOpacity, bar[HORIZONTAL]->layer()->opacity());
  EXPECT_EQ(kMaxOpacity, bar[VERTICAL]->layer()->opacity());
  EXPECT_FALSE(hide_timer[HORIZONTAL]->IsRunning());
  EXPECT_FALSE(hide_timer[VERTICAL]->IsRunning());

  generator.CancelTrackpadRest();
  // Scrollers should start fading out, but only after a delay.
  for (ScrollBarOrientation orientation : {HORIZONTAL, VERTICAL}) {
    EXPECT_EQ(kMaxOpacity, bar[orientation]->layer()->GetTargetOpacity());
    EXPECT_TRUE(hide_timer[orientation]->IsRunning());
    // Trigger the timer. Should then be fading out.
    hide_timer[orientation]->user_task().Run();
    hide_timer[orientation]->Stop();
    EXPECT_EQ(0, bar[orientation]->layer()->GetTargetOpacity());
  }

  // Rest again.
  generator.GenerateTrackpadRest();
  EXPECT_EQ(kMaxOpacity, bar[HORIZONTAL]->layer()->GetTargetOpacity());
  EXPECT_EQ(kMaxOpacity, bar[VERTICAL]->layer()->GetTargetOpacity());

  // Scroll vertically.
  const float y_offset = 3;
  const int kSteps = 1;
  const int kNnumFingers = 2;
  generator.ScrollSequence(generator.current_screen_location(),
                           base::TimeDelta(), 0, y_offset, kSteps,
                           kNnumFingers);

  // Horizontal scroller should start fading out immediately.
  EXPECT_EQ(kMaxOpacity, bar[HORIZONTAL]->layer()->opacity());
  EXPECT_EQ(0, bar[HORIZONTAL]->layer()->GetTargetOpacity());
  EXPECT_FALSE(hide_timer[HORIZONTAL]->IsRunning());

  // Vertical should remain visible, but ready to fade out after a delay.
  EXPECT_EQ(kMaxOpacity, bar[VERTICAL]->layer()->opacity());
  EXPECT_EQ(kMaxOpacity, bar[VERTICAL]->layer()->GetTargetOpacity());
  EXPECT_TRUE(hide_timer[VERTICAL]->IsRunning());

  // Scrolling should have occurred.
  EXPECT_EQ(gfx::PointF(0, y_offset), test_api.CurrentOffset());

  // Then, scrolling horizontally should show the horizontal scroller. The
  // vertical scroller should still be visible, running its hide timer.
  const float x_offset = 5;
  generator.ScrollSequence(generator.current_screen_location(),
                           base::TimeDelta(), x_offset, 0, kSteps,
                           kNnumFingers);
  for (ScrollBarOrientation orientation : {HORIZONTAL, VERTICAL}) {
    EXPECT_EQ(kMaxOpacity, bar[orientation]->layer()->opacity());
    EXPECT_EQ(kMaxOpacity, bar[orientation]->layer()->GetTargetOpacity());
    EXPECT_TRUE(hide_timer[orientation]->IsRunning());
  }

  // Now scrolling has occurred in both directions.
  EXPECT_EQ(gfx::PointF(x_offset, y_offset), test_api.CurrentOffset());
}

#endif  // BUILDFLAG(IS_MAC)

// Test that increasing the size of the viewport "below" scrolled content causes
// the content to scroll up so that it still fills the viewport.
TEST_F(ScrollViewTest, ConstrainScrollToBounds) {}

// Calling Layout on ScrollView should not reset the scroll location.
TEST_F(ScrollViewTest, ContentScrollNotResetOnLayout) {}

TEST_F(ScrollViewTest, ArrowKeyScrolling) {}

TEST_F(ScrollViewTest, ArrowKeyScrollingDisabled) {}

// Test that overflow indicators turn on appropriately.
TEST_F(ScrollViewTest, VerticalOverflowIndicators) {}

TEST_F(ScrollViewTest, HorizontalOverflowIndicators) {}

TEST_F(ScrollViewTest, HorizontalVerticalOverflowIndicators) {}

TEST_F(ScrollViewTest, VerticalWithHeaderOverflowIndicators) {}

TEST_F(ScrollViewTest, CustomOverflowIndicator) {}

// Ensure ScrollView::Layout succeeds if a disabled scrollbar's overlap style
// does not match the other scrollbar.
TEST_F(ScrollViewTest, IgnoreOverlapWithDisabledHorizontalScroll) {}

// Ensure ScrollView::Layout succeeds if a hidden but enabled scrollbar's
// overlap style does not match the other scrollbar.
TEST_F(ScrollViewTest, IgnoreOverlapWithHiddenHorizontalScroll) {}

// Ensure ScrollView::Layout succeeds if a disabled scrollbar's overlap style
// does not match the other scrollbar.
TEST_F(ScrollViewTest, IgnoreOverlapWithDisabledVerticalScroll) {}

// Ensure ScrollView::Layout succeeds if a hidden but enabled scrollbar's
// overlap style does not match the other scrollbar.
TEST_F(ScrollViewTest, IgnoreOverlapWithHiddenVerticalScroll) {}

TEST_F(ScrollViewTest, TestSettingContentsToNull) {}

// Test scrolling behavior when clicking on the scroll track.
TEST_F(WidgetScrollViewTest, ScrollTrackScrolling) {}

// Test that LocatedEvents are transformed correctly when scrolling.
TEST_F(WidgetScrollViewTest, EventLocation) {}

// Ensure behavior of ScrollRectToVisible() is consistent when scrolling with
// and without layers, and under LTR and RTL.
TEST_P(WidgetScrollViewTestRTLAndLayers, ScrollOffsetWithoutLayers) {}

// Test that views scroll offsets are in sync with the layer scroll offsets.
TEST_P(WidgetScrollViewTestRTLAndLayers, ScrollOffsetUsingLayers) {}

namespace {

// Applies |scroll_event| to |scroll_view| and verifies that the event is
// applied correctly whether or not compositor scrolling is enabled.
static void ApplyScrollEvent(const ScrollViewTestApi& test_api,
                             ScrollView* scroll_view,
                             ui::ScrollEvent& scroll_event) {}

}  // namespace

// Tests to see the scroll events are handled correctly in composited and
// non-composited scrolling.
TEST_F(WidgetScrollViewTest, CompositedScrollEvents) {}

// Tests to see that transposed (treat-as-horizontal) scroll events are handled
// correctly in composited and non-composited scrolling.
TEST_F(WidgetScrollViewTest, CompositedTransposedScrollEvents) {}

// Tests to see that transposed (treat-as-horizontal) scroll events are handled
// correctly in composited and non-composited scrolling when the scroll offset
// is somewhat ambiguous. This is the case where the horizontal component is
// larger than the vertical.
TEST_F(WidgetScrollViewTest,
       DISABLED_CompositedTransposedScrollEventsHorizontalComponentIsLarger) {}

// Tests to see that transposed (treat-as-horizontal) scroll events are handled
// correctly in composited and non-composited scrolling when the scroll offset
// is somewhat ambiguous. This is the case where the vertical component is
// larger than the horizontal.
TEST_F(WidgetScrollViewTest,
       CompositedTransposedScrollEventsVerticalComponentIsLarger) {}

TEST_F(WidgetScrollViewTest, UnboundedScrollViewUsesContentPreferredSize) {}

INSTANTIATE_TEST_SUITE_P();

}  // namespace views