chromium/ash/controls/rounded_scroll_bar_unittest.cc

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

#include <memory>

#include "ash/controls/rounded_scroll_bar.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/compositor/layer.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
#include "ui/views/controls/scrollbar/scroll_bar.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_utils.h"

namespace ash {
namespace {

// Scroll bar configuration.
constexpr int kScrollBarWidth = 10;
constexpr int kViewportHeight = 200;
constexpr int kContentHeight = 1000;

// Scroll bar thumb thickness.
constexpr int kThumbThickness = 8;
constexpr int kThumbHoverInset = 2;

// Thumb opacity values.
constexpr float kDefaultOpacity = 0.38f;
constexpr float kActiveOpacity = 1.0f;

// A no-op controller.
class TestScrollBarController : public views::ScrollBarController {
 public:
  // views::ScrollBarController:
  void ScrollToPosition(views::ScrollBar* source, int position) override {}
  int GetScrollIncrement(views::ScrollBar* source,
                         bool is_page,
                         bool is_positive) override {
    return 0;
  }
};

// Uses ViewsTestBase because we may want to move this control into //ui/views
// in the future.
class RoundedScrollBarTest : public views::ViewsTestBase,
                             public testing::WithParamInterface<bool> {
 public:
  RoundedScrollBarTest()
      : ViewsTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ~RoundedScrollBarTest() override = default;

  // testing::Test:
  void SetUp() override {
    if (GetParam()) {
      scoped_feature_list_.InitWithFeatures(
          {chromeos::features::kJelly, chromeos::features::kJellyroll}, {});
    } else {
      scoped_feature_list_.InitWithFeatures(
          {}, {chromeos::features::kJelly, chromeos::features::kJellyroll});
    }

    ViewsTestBase::SetUp();

    // Create a small widget.
    widget_ = std::make_unique<views::Widget>();
    views::Widget::InitParams params =
        CreateParams(views::Widget::InitParams::TYPE_POPUP);
    params.bounds = gfx::Rect(10, 10, 100, 200);
    widget_->Init(std::move(params));
    widget_->Show();

    // Add a vertical scrollbar along the right edge.
    auto* contents = widget_->SetContentsView(std::make_unique<views::View>());
    scroll_bar_ = contents->AddChildView(std::make_unique<RoundedScrollBar>(
        views::ScrollBar::Orientation::kVertical));
    scroll_bar_->set_controller(&controller_);
    scroll_bar_->SetBounds(90, 0, kScrollBarWidth, kViewportHeight);
    scroll_bar_->Update(kViewportHeight, kContentHeight,
                        /*contents_scroll_offset=*/0);
    thumb_ = scroll_bar_->GetThumbForTest();
    generator_ = std::make_unique<ui::test::EventGenerator>(
        GetRootWindow(widget_.get()));
  }

  void TearDown() override {
    widget_.reset();
    ViewsTestBase::TearDown();
  }

 protected:
  views::UniqueWidgetPtr widget_;
  TestScrollBarController controller_;
  raw_ptr<RoundedScrollBar, DanglingUntriaged> scroll_bar_ = nullptr;
  raw_ptr<views::BaseScrollBarThumb, DanglingUntriaged> thumb_ = nullptr;
  std::unique_ptr<ui::test::EventGenerator> generator_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All, RoundedScrollBarTest, testing::Bool());

TEST_P(RoundedScrollBarTest, InvisibleByDefault) {
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), 0.f);
}

TEST_P(RoundedScrollBarTest, ShowOnThumbBoundsChanged) {
  // Programmatically scroll the view, which changes the thumb bounds.
  // By default this does not show the thumb.
  scroll_bar_->Update(kViewportHeight, kContentHeight,
                      /*contents_scroll_offset=*/100);
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), 0.f);

  // With the setting enabled, changing the thumb bounds shows the thumb.
  scroll_bar_->SetShowOnThumbBoundsChanged(true);
  scroll_bar_->Update(kViewportHeight, kContentHeight,
                      /*contents_scroll_offset=*/200);
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(),
            GetParam() ? kActiveOpacity : kDefaultOpacity);
}

TEST_P(RoundedScrollBarTest, ShowOnScrolling) {
  scroll_bar_->ScrollByAmount(views::ScrollBar::ScrollAmount::kNextLine);
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(),
            GetParam() ? kActiveOpacity : kDefaultOpacity);
}

TEST_P(RoundedScrollBarTest, FadesAfterScroll) {
  scroll_bar_->ScrollByAmount(views::ScrollBar::ScrollAmount::kNextLine);
  task_environment()->FastForwardBy(base::Seconds(1));
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), 0.f);
}

TEST_P(RoundedScrollBarTest, AlwaysShowThumbIsTrue) {
  scroll_bar_->SetAlwaysShowThumb(true);
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(),
            GetParam() ? kActiveOpacity : kDefaultOpacity);
  scroll_bar_->ScrollByAmount(views::ScrollBar::ScrollAmount::kNextLine);
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(),
            GetParam() ? kActiveOpacity : kDefaultOpacity);
  generator_->MoveMouseTo(thumb_->GetBoundsInScreen().CenterPoint());
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), kActiveOpacity);
}

TEST_P(RoundedScrollBarTest, MoveToThumbShowsActiveOpacity) {
  generator_->MoveMouseTo(thumb_->GetBoundsInScreen().CenterPoint());
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), kActiveOpacity);
}

TEST_P(RoundedScrollBarTest, MoveToTrackOutsideThumbShowsInactiveThumb) {
  gfx::Point thumb_bottom = thumb_->GetBoundsInScreen().bottom_center();
  generator_->MoveMouseTo(thumb_bottom.x(), thumb_bottom.y() + 1);

  if (GetParam()) {
    EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), 1.0f);
    EXPECT_EQ(scroll_bar_->GetThickness(), kThumbThickness - kThumbHoverInset);
  } else {
    EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), kDefaultOpacity);
    EXPECT_EQ(scroll_bar_->GetThickness(), kThumbThickness);
  }
}

TEST_P(RoundedScrollBarTest, MoveFromThumbToTrackShowsInactiveThumb) {
  generator_->MoveMouseTo(thumb_->GetBoundsInScreen().CenterPoint());
  gfx::Point thumb_bottom = thumb_->GetBoundsInScreen().bottom_center();
  generator_->MoveMouseTo(thumb_bottom.x(), thumb_bottom.y() + 1);
  if (GetParam()) {
    EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), 1.0f);
    EXPECT_EQ(scroll_bar_->GetThickness(), kThumbThickness - kThumbHoverInset);
  } else {
    EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), kDefaultOpacity);
    EXPECT_EQ(scroll_bar_->GetThickness(), kThumbThickness);
  }
}

TEST_P(RoundedScrollBarTest, MoveFromTrackToThumbShowsActiveThumb) {
  gfx::Point thumb_bottom = thumb_->GetBoundsInScreen().bottom_center();
  generator_->MoveMouseTo(thumb_bottom.x(), thumb_bottom.y() + 1);
  generator_->MoveMouseTo(thumb_->GetBoundsInScreen().CenterPoint());
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), kActiveOpacity);
  EXPECT_EQ(scroll_bar_->GetThickness(), kThumbThickness);
}

TEST_P(RoundedScrollBarTest, DragOutsideTrackShowsActiveThumb) {
  gfx::Point thumb_center = thumb_->GetBoundsInScreen().CenterPoint();
  generator_->MoveMouseTo(thumb_center);
  generator_->PressLeftButton();
  gfx::Point outside_scroll_bar(thumb_center.x() + kScrollBarWidth,
                                thumb_center.y());
  generator_->MoveMouseTo(outside_scroll_bar);
  EXPECT_EQ(thumb_->layer()->GetTargetOpacity(), kActiveOpacity);
  EXPECT_EQ(scroll_bar_->GetThickness(), kThumbThickness);
}

}  // namespace
}  // namespace ash