chromium/ash/system/focus_mode/focus_mode_chip_carousel_unittest.cc

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

#include "ash/system/focus_mode/focus_mode_chip_carousel.h"

#include <vector>

#include "ash/api/tasks/tasks_types.h"
#include "ash/constants/ash_features.h"
#include "ash/system/focus_mode/focus_mode_tasks_provider.h"
#include "ash/test/ash_test_base.h"
#include "base/i18n/rtl.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/compositor/layer.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"

namespace {

MATCHER_P(LabelMatcher, task, "") {
  return static_cast<views::LabelButton*>(arg)->GetText() == task;
}

std::vector<std::string> kTestTaskTitles = {"Preparing for I485 form",
                                            "Podcast interview Script",
                                            "Book a flight to Seoul"};

constexpr int kWidgetWidth = 320;
constexpr float kGradientWidth = 16;

}  // namespace

namespace ash {

class FocusModeChipCarouselTest : public AshTestBase {
 public:
  FocusModeChipCarouselTest() : scoped_feature_(features::kFocusMode) {}
  ~FocusModeChipCarouselTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    widget_ = CreateFramelessTestWidget();
    widget_->SetBounds(gfx::Rect(/*width=*/kWidgetWidth, /*height=*/48));

    focus_mode_chip_carousel_ = widget_->SetContentsView(
        std::make_unique<FocusModeChipCarousel>(base::DoNothing()));
  }

  void TearDown() override {
    focus_mode_chip_carousel_ = nullptr;
    widget_.reset();
    AshTestBase::TearDown();
  }

  std::vector<FocusModeTask> GetTasks(const std::vector<std::string>& titles) {
    std::vector<FocusModeTask> tasks;

    base::Time updated = base::Time::Now();
    for (size_t i = 0; i != titles.size(); ++i) {
      FocusModeTask& task = tasks.emplace_back();
      task.task_id = {.list_id = "task_list_id", .id = base::NumberToString(i)};
      task.title = titles[i];
      task.updated = updated - base::Seconds(i);
    }

    return tasks;
  }

  FocusModeChipCarousel* focus_mode_chip_carousel() {
    return focus_mode_chip_carousel_;
  }

  views::ScrollView* GetScrollView() {
    return focus_mode_chip_carousel_->GetScrollViewForTesting();
  }

  views::View* GetScrollContents() {
    return focus_mode_chip_carousel_->GetScrollViewForTesting()->contents();
  }

  views::ImageButton* GetLeftOverflowIcon() {
    return focus_mode_chip_carousel_->left_overflow_icon_;
  }

  views::ImageButton* GetRightOverflowIcon() {
    return focus_mode_chip_carousel_->right_overflow_icon_;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_;
  std::unique_ptr<views::Widget> widget_;
  raw_ptr<FocusModeChipCarousel> focus_mode_chip_carousel_;
};

// Tests that the task list displays the list of tasks.
TEST_F(FocusModeChipCarouselTest, ChipCarouselPopulates) {
  EXPECT_FALSE(focus_mode_chip_carousel()->HasTasks());
  auto validate_tasks = [&](const std::vector<std::string> task_titles) {
    SCOPED_TRACE(::testing::Message()
                 << "Tasks length: " << task_titles.size());
    focus_mode_chip_carousel()->SetTasks(GetTasks(task_titles));
    EXPECT_EQ(task_titles.size(),
              GetScrollContents()->GetChildrenInZOrder().size());
    EXPECT_NE(task_titles.empty(), focus_mode_chip_carousel()->HasTasks());

    std::vector<LabelMatcherMatcherP<std::u16string>> task_labels = {};
    for (const std::string& task : task_titles) {
      task_labels.push_back(LabelMatcher(base::UTF8ToUTF16(task)));
    }

    EXPECT_THAT(GetScrollContents()->GetChildrenInZOrder(),
                testing::ElementsAreArray(task_labels));
  };

  validate_tasks({});
  validate_tasks(kTestTaskTitles);
  validate_tasks({"Only one task"});
  validate_tasks({"Maximum", "of", "five", "tasks", "populated"});
}

// Tests that if more than 5 tasks are provided, the carousel only populates the
// first 5.
TEST_F(FocusModeChipCarouselTest, MaxOfFive) {
  focus_mode_chip_carousel()->SetTasks(
      GetTasks({"one", "two", "three", "four", "five", "six"}));
  EXPECT_EQ(5u, GetScrollContents()->GetChildrenInZOrder().size());

  // The first 5 tasks should be populated.
  std::vector<LabelMatcherMatcherP<std::u16string>> task_labels = {};
  for (const std::string& task_title :
       {"one", "two", "three", "four", "five"}) {
    task_labels.push_back(LabelMatcher(base::UTF8ToUTF16(task_title)));
  }
  EXPECT_THAT(GetScrollContents()->GetChildrenInZOrder(),
              testing::ElementsAreArray(task_labels));
}

// Tests that the gradient exists on sides of the scroll that are overflowed,
// and that the overflow buttons exist on those sides when hovered.
TEST_F(FocusModeChipCarouselTest, GradientOnScroll) {
  // The scroll view should be by default empty with no gradient.
  EXPECT_FALSE(GetScrollView()->layer()->HasGradientMask());

  // Setting 1 task shouldn't make the scroll view overflow, so there should
  // still be no gradient.
  focus_mode_chip_carousel()->SetTasks(GetTasks({"Preparing for I485 form"}));
  views::test::RunScheduledLayout(focus_mode_chip_carousel());
  EXPECT_FALSE(GetScrollView()->layer()->HasGradientMask());

  // Three tasks should overflow the scroll view and the gradient should appear.
  focus_mode_chip_carousel()->SetTasks(GetTasks(kTestTaskTitles));
  views::test::RunScheduledLayout(focus_mode_chip_carousel());
  EXPECT_TRUE(GetScrollView()->layer()->HasGradientMask());

  // Neither overflow button should be visible before hovering.
  EXPECT_FALSE(GetLeftOverflowIcon()->GetVisible());
  EXPECT_FALSE(GetRightOverflowIcon()->GetVisible());

  // Hovering should make the right overflow button appear.
  GetEventGenerator()->MoveMouseTo(
      focus_mode_chip_carousel()->GetBoundsInScreen().CenterPoint());
  EXPECT_FALSE(GetLeftOverflowIcon()->GetVisible());
  EXPECT_TRUE(GetRightOverflowIcon()->GetVisible());

  // Clicking the right overflow button should make both overflow buttons
  // appear, now that the left side should be overflown.
  GetEventGenerator()->MoveMouseTo(
      GetRightOverflowIcon()->GetBoundsInScreen().CenterPoint());
  GetEventGenerator()->ClickLeftButton();
  EXPECT_TRUE(GetLeftOverflowIcon()->GetVisible());
  EXPECT_TRUE(GetRightOverflowIcon()->GetVisible());

  // Both overflow icons should be shown on top of the scroll view.
  EXPECT_EQ(gfx::Size(kWidgetWidth, 32),
            GetScrollView()->GetBoundsInScreen().size());
  EXPECT_EQ(gfx::Size(28, 32),
            GetLeftOverflowIcon()->GetBoundsInScreen().size());
  EXPECT_EQ(gfx::Size(28, 32),
            GetRightOverflowIcon()->GetBoundsInScreen().size());
  EXPECT_TRUE(
      GetLeftOverflowIcon()->HitTestRect(views::View::ConvertRectToTarget(
          GetScrollContents(), GetLeftOverflowIcon(),
          GetScrollView()->GetVisibleRect())));
  EXPECT_TRUE(
      GetRightOverflowIcon()->HitTestRect(views::View::ConvertRectToTarget(
          GetScrollContents(), GetRightOverflowIcon(),
          GetScrollView()->GetVisibleRect())));
}

// Tests that the gradient shows up on the correct side in RTL.
TEST_F(FocusModeChipCarouselTest, GradientInRTL) {
  base::i18n::SetRTLForTesting(true);

  focus_mode_chip_carousel()->SetTasks(GetTasks(kTestTaskTitles));
  views::test::RunScheduledLayout(focus_mode_chip_carousel());
  EXPECT_TRUE(GetScrollView()->layer()->HasGradientMask());

  // In RTL the carousel starts on the right side, so we can only scroll to the
  // left and not to the right. Because of this the gradient should only be
  // shown on the left side.
  ASSERT_EQ(2u, GetScrollView()->layer()->gradient_mask().step_count());
  auto steps = GetScrollView()->layer()->gradient_mask().steps();
  const float allowed_difference = 0.0001f;

  EXPECT_FLOAT_EQ(0.0f, steps.front().fraction);
  EXPECT_EQ(0u, steps.front().alpha);
  EXPECT_NEAR(kGradientWidth / kWidgetWidth, steps[1].fraction,
              allowed_difference);
  EXPECT_EQ(255u, steps[1].alpha);
}

}  // namespace ash