chromium/ash/glanceables/glanceables_unittest.cc

// Copyright 2024 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/constants/ash_features.h"
#include "ash/glanceables/classroom/fake_glanceables_classroom_client.h"
#include "ash/glanceables/classroom/glanceables_classroom_student_view.h"
#include "ash/glanceables/common/glanceables_contents_scroll_view.h"
#include "ash/glanceables/common/glanceables_time_management_bubble_view.h"
#include "ash/glanceables/common/glanceables_util.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/glanceables/glanceables_controller.h"
#include "ash/glanceables/tasks/glanceables_tasks_view.h"
#include "ash/glanceables/tasks/test/glanceables_tasks_test_util.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/shell.h"
#include "ash/style/counter_expand_button.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/system/unified/date_tray.h"
#include "ash/system/unified/glanceable_tray_bubble.h"
#include "ash/system/unified/glanceable_tray_bubble_view.h"
#include "ash/test/ash_test_base.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/types/cxx23_to_underlying.h"
#include "components/account_id/account_id.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/test/widget_animation_waiter.h"
#include "ui/views/view_utils.h"

namespace ash {

namespace {

class ResizeAnimationWaiter {
 public:
  explicit ResizeAnimationWaiter(
      GlanceablesTimeManagementBubbleView* bubble_view)
      : bubble_view_(bubble_view) {}
  ResizeAnimationWaiter(const ResizeAnimationWaiter&) = delete;
  ResizeAnimationWaiter& operator=(const ResizeAnimationWaiter&) = delete;
  ~ResizeAnimationWaiter() = default;

  void Wait() {
    // Only run the `run_loop_` if the bubble is animating.
    if (bubble_view_->is_animating_resize()) {
      base::RunLoop run_loop;
      bubble_view_->SetAnimationEndedClosureForTest(run_loop.QuitClosure());
      run_loop.Run();
    }

    // Force frames and wait for all throughput trackers to be gone to allow
    // animation throughput data to be passed from cc to ui.
    ui::Compositor* compositor = bubble_view_->GetWidget()->GetCompositor();
    while (compositor->has_throughput_trackers_for_testing()) {
      compositor->ScheduleFullRedraw();
      std::ignore = ui::WaitForNextFrameToBePresented(compositor,
                                                      base::Milliseconds(500));
    }
  }

 private:
  raw_ptr<GlanceablesTimeManagementBubbleView> bubble_view_;
  base::WeakPtrFactory<ResizeAnimationWaiter> weak_ptr_factory_{this};
};

}  // namespace

class GlanceablesBaseTest : public AshTestBase {
 public:
  GlanceablesBaseTest() {
    features_.InitWithFeatures(
        /*enabled_features=*/
        {features::kGlanceablesTimeManagementTasksView,
         features::kGlanceablesTimeManagementClassroomStudentView},
        /*disabled_features=*/{});
  }

  void SetUp() override {
    AshTestBase::SetUp();

    const auto account_id =
        AccountId::FromUserEmailGaiaId("[email protected]", "123456");
    SimulateUserLogin(account_id);

    classroom_client_ = std::make_unique<FakeGlanceablesClassroomClient>();
    tasks_client_ = glanceables_tasks_test_util::InitializeFakeTasksClient(
        base::Time::Now());
    tasks_client_->set_http_error(google_apis::ApiErrorCode::HTTP_SUCCESS);
    Shell::Get()->glanceables_controller()->UpdateClientsRegistration(
        account_id, GlanceablesController::ClientsRegistration{
                        .classroom_client = classroom_client_.get(),
                        .tasks_client = tasks_client_.get()});
    ASSERT_TRUE(Shell::Get()->glanceables_controller()->GetClassroomClient());
    ASSERT_TRUE(Shell::Get()->glanceables_controller()->GetTasksClient());
  }

  FakeGlanceablesClassroomClient* classroom_client() const {
    return classroom_client_.get();
  }
  api::FakeTasksClient* tasks_client() const { return tasks_client_.get(); }
  DateTray* date_tray() const {
    return StatusAreaWidgetTestHelper::GetStatusAreaWidget()->date_tray();
  }

 private:
  base::test::ScopedFeatureList features_;
  std::unique_ptr<FakeGlanceablesClassroomClient> classroom_client_;
  std::unique_ptr<api::FakeTasksClient> tasks_client_;
};

TEST_F(GlanceablesBaseTest, DoesNotAddClassroomViewWhenDisabledByAdmin) {
  // Open Glanceables via Search + C, make sure the bubble is shown with the
  // Classroom view available.
  EXPECT_FALSE(date_tray()->is_active());
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));
  EXPECT_TRUE(date_tray()->is_active());
  EXPECT_TRUE(
      date_tray()->glanceables_bubble_for_test()->GetClassroomStudentView());

  // Close Glanceables.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));

  // Simulate that admin disables the integration.
  classroom_client()->set_is_disabled_by_admin(true);

  // Open Glanceables via Search + C again, make sure the bubble no longer
  // contains the Classroom view.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));
  EXPECT_TRUE(date_tray()->is_active());
  EXPECT_FALSE(
      date_tray()->glanceables_bubble_for_test()->GetClassroomStudentView());

  // Close Glanceables.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));
}

TEST_F(GlanceablesBaseTest, DoesNotAddTasksViewWhenDisabledByAdmin) {
  // Open Glanceables via Search + C, make sure the bubble is shown with the
  // Tasks view available.
  EXPECT_FALSE(date_tray()->is_active());
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));
  EXPECT_TRUE(date_tray()->is_active());
  EXPECT_TRUE(date_tray()->glanceables_bubble_for_test()->GetTasksView());

  // Close Glanceables.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));

  // Simulate that admin disables the integration.
  tasks_client()->set_is_disabled_by_admin(true);

  // Open Glanceables via Search + C again, make sure the bubble no longer
  // contains the Tasks view.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));
  EXPECT_TRUE(date_tray()->is_active());
  EXPECT_FALSE(date_tray()->glanceables_bubble_for_test()->GetTasksView());

  // Close Glanceables.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_C, ui::EF_COMMAND_DOWN));
}

class GlanceablesTasksAndClassroomTest : public GlanceablesBaseTest {
 public:
  void SetUp() override {
    GlanceablesBaseTest::SetUp();

    date_tray()->ShowGlanceableBubble(/*from_keyboard=*/false);
    view_ = views::AsViewClass<GlanceableTrayBubbleView>(
        date_tray()->glanceables_bubble_for_test()->GetBubbleView());

    glanceables_util::SetIsNetworkConnectedForTest(true);
  }

  void TearDown() override {
    date_tray()->HideGlanceableBubble();
    view_ = nullptr;
    AshTestBase::TearDown();
  }

  void ReopenGlanceables() {
    date_tray()->HideGlanceableBubble();
    view_ = nullptr;
    date_tray()->ShowGlanceableBubble(/*from_keyboard=*/false);
    view_ = views::AsViewClass<GlanceableTrayBubbleView>(
        date_tray()->glanceables_bubble_for_test()->GetBubbleView());
  }

  // Populates `num` of tasks to the default task list.
  void PopulateTasks(size_t num) {
    for (size_t i = 0; i < num; ++i) {
      auto num_string = base::NumberToString(i);
      tasks_client()->AddTask("TaskListID1",
                              base::StrCat({"title_", num_string}),
                              base::DoNothing());
    }

    // Simulate closing the glanceables bubble to cache the tasks.
    tasks_client()->OnGlanceablesBubbleClosed(base::DoNothing());

    // Recreate the tasks view to update the task views.
    date_tray()->ShowGlanceableBubble(/*from_keyboard=*/false);
    view_ = views::AsViewClass<GlanceableTrayBubbleView>(
        date_tray()->glanceables_bubble_for_test()->GetBubbleView());
  }

  void GenerateGestureScrollEvent(gfx::Point gesture_position,
                                  bool upward,
                                  int distance_to_scroll) {
    // To move the scroll view contents upward, we need to scroll the gesture
    // downward.
    const gfx::Vector2d scroll_distance =
        upward ? gfx::Vector2d(0, distance_to_scroll)
               : gfx::Vector2d(0, -distance_to_scroll);
    GetEventGenerator()->GestureScrollSequence(
        gesture_position, gesture_position + scroll_distance,
        base::Milliseconds(100), /*steps=*/10);
  }

  void GenerateTrackpadScrollEvent(gfx::Point mouse_position,
                                   bool upward,
                                   int distance_to_scroll) {
    GetEventGenerator()->ScrollSequence(
        mouse_position, base::TimeDelta(), /*x_offset=*/0,
        upward ? distance_to_scroll : -distance_to_scroll, /*steps=*/1,
        /*num_fingers=*/2);
  }

  GlanceablesTasksView* GetTasksView() const {
    return views::AsViewClass<GlanceablesTasksView>(view_->GetTasksView());
  }

  CounterExpandButton* GetTasksExpandButtonView() const {
    return views::AsViewClass<CounterExpandButton>(
        GetTasksView()->GetViewByID(base::to_underlying(
            GlanceablesViewId::kTimeManagementBubbleExpandButton)));
  }

  views::ScrollView* GetTasksScrollView() const {
    return views::AsViewClass<views::ScrollView>(GetTasksView()->GetViewByID(
        base::to_underlying(GlanceablesViewId::kContentsScrollView)));
  }

  GlanceablesClassroomStudentView* GetClassroomView() const {
    return views::AsViewClass<GlanceablesClassroomStudentView>(
        view_->GetClassroomStudentView());
  }

  CounterExpandButton* GetClassroomExpandButtonView() const {
    return views::AsViewClass<CounterExpandButton>(
        GetClassroomView()->GetViewByID(base::to_underlying(
            GlanceablesViewId::kTimeManagementBubbleExpandButton)));
  }

  views::ScrollView* GetClassroomScrollView() const {
    return views::AsViewClass<views::ScrollView>(
        GetClassroomView()->GetViewByID(
            base::to_underlying(GlanceablesViewId::kContentsScrollView)));
  }

  GlanceableTrayBubbleView* view() const { return view_; }

 private:
  raw_ptr<GlanceableTrayBubbleView> view_ = nullptr;
};

TEST_F(GlanceablesTasksAndClassroomTest, Basics) {
  auto* const tasks_view = GetTasksView();
  EXPECT_TRUE(tasks_view);
  auto* const classroom_view = GetClassroomView();
  EXPECT_TRUE(classroom_view);

  // Check that both views have their own backgrounds.
  EXPECT_TRUE(tasks_view->GetBackground());
  EXPECT_TRUE(classroom_view->GetBackground());

  // Check that both views contain their expand buttons.
  EXPECT_TRUE(GetTasksExpandButtonView());
  EXPECT_TRUE(GetClassroomExpandButtonView());
}

TEST_F(GlanceablesTasksAndClassroomTest, TimeManagementExpandStates) {
  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();

  // Initially `tasks_view` is expanded and `classroom_view` is collapsed.
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Verify the expand states of the buttons.
  auto* const tasks_expand_button = GetTasksExpandButtonView();
  auto* const classroom_expand_button = GetClassroomExpandButtonView();
  ASSERT_TRUE(tasks_expand_button);
  ASSERT_TRUE(classroom_expand_button);
  EXPECT_TRUE(tasks_expand_button->expanded());
  EXPECT_FALSE(classroom_expand_button->expanded());
  EXPECT_EQ(tasks_expand_button->GetTooltipText(), u"Collapse Google Tasks");
  EXPECT_EQ(classroom_expand_button->GetTooltipText(),
            u"Expand Google Classroom");

  // Expanding/Collapsing `tasks_view` will collapse/expand `classroom_view`.
  LeftClickOn(tasks_expand_button);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());
  EXPECT_EQ(tasks_expand_button->GetTooltipText(), u"Expand Google Tasks");
  EXPECT_EQ(classroom_expand_button->GetTooltipText(),
            u"Collapse Google Classroom");

  LeftClickOn(tasks_expand_button);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
  EXPECT_EQ(tasks_expand_button->GetTooltipText(), u"Collapse Google Tasks");
  EXPECT_EQ(classroom_expand_button->GetTooltipText(),
            u"Expand Google Classroom");

  // Same for `classroom_view`.
  LeftClickOn(classroom_expand_button);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());
  EXPECT_EQ(tasks_expand_button->GetTooltipText(), u"Expand Google Tasks");
  EXPECT_EQ(classroom_expand_button->GetTooltipText(),
            u"Collapse Google Classroom");

  LeftClickOn(classroom_expand_button);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
  EXPECT_EQ(tasks_expand_button->GetTooltipText(), u"Collapse Google Tasks");
  EXPECT_EQ(classroom_expand_button->GetTooltipText(),
            u"Expand Google Classroom");
}

TEST_F(GlanceablesTasksAndClassroomTest,
       TrackpadScrollingDownFromTheBottomOfTasksExpandsClassroom) {
  // Increase the number of tasks to ensure the scroll contents overflow.
  PopulateTasks(10);

  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const tasks_scroll_bar = GetTasksScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(tasks_scroll_bar->GetVisible());
  const gfx::Point tasks_scroll_view_center =
      GetTasksScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = tasks_scroll_bar->GetMaxPosition() -
                                 tasks_scroll_bar->GetMinPosition() + 10;

  // Scrolling upward at the top of the scroll view doesn't change expand state.
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/true,
                              distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward when there is scrollable content doesn't change expand
  // state.
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward at the bottom of the scroll view changes expand state.
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest,
       GestureScrollingDownFromTheBottomOfTasksExpandsClassroom) {
  // Increase the number of tasks to ensure the scroll contents overflow.
  PopulateTasks(10);

  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const tasks_scroll_bar = GetTasksScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(tasks_scroll_bar->GetVisible());
  const gfx::Point tasks_scroll_view_center =
      GetTasksScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = tasks_scroll_bar->GetMaxPosition() -
                                 tasks_scroll_bar->GetMinPosition() + 10;

  // Scrolling upward at the top of the scroll view doesn't change expand state.
  GenerateGestureScrollEvent(tasks_scroll_view_center, /*upward=*/true,
                             distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward when there is scrollable content doesn't change expand
  // state.
  GenerateGestureScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                             distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward at the bottom of the scroll view changes expand state.
  GenerateGestureScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                             distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest,
       MouseWheelScrollingDownFromTheBottomOfTasksExpandsClassroom) {
  // Increase the number of tasks to ensure the scroll contents overflow.
  PopulateTasks(10);

  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const tasks_scroll_bar = GetTasksScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(tasks_scroll_bar->GetVisible());
  const gfx::Point tasks_scroll_view_center =
      GetTasksScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = tasks_scroll_bar->GetMaxPosition() -
                                 tasks_scroll_bar->GetMinPosition() + 10;

  // Scrolling upward at the top of the scroll view doesn't change expand state.
  GetEventGenerator()->MoveMouseTo(tasks_scroll_view_center);
  GetEventGenerator()->MoveMouseWheel(0, distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward when there is scrollable content doesn't change expand
  // state.
  GetEventGenerator()->MoveMouseWheel(0, -distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Right after hitting the bottom of the scroll view, scrolling downward at
  // the bottom of the scroll view doesn't change expand state.
  GetEventGenerator()->MoveMouseWheel(0, -distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // After the mouse wheel is fired, scrolling downward at the bottom of the
  // scroll view changes expand state.
  views::AsViewClass<GlanceablesContentsScrollView>(GetTasksScrollView())
      ->FireMouseWheelTimerForTest();
  GetEventGenerator()->MoveMouseWheel(0, -distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest,
       TrackpadScrollingUpFromTheTopOfClassroomExpandsTasks) {
  // Expand classroom first to make the scroll view visible.
  auto const* classroom_expand_button = GetClassroomExpandButtonView();
  ASSERT_TRUE(classroom_expand_button);
  LeftClickOn(classroom_expand_button);
  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const classroom_scroll_bar =
      GetClassroomScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(classroom_scroll_bar->GetVisible());
  const gfx::Point classroom_scroll_view_center =
      GetClassroomScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = classroom_scroll_bar->GetMaxPosition() -
                                 classroom_scroll_bar->GetMinPosition() + 10;

  // Scrolling downward to the bottom of the scroll view doesn't change expand
  // state.
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward when there is scrollable content doesn't change expand
  // state.
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                              distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward at the top of the scroll view changes expand state.
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                              distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest,
       GestureScrollingUpFromTheTopOfClassroomExpandsTasks) {
  // Expand classroom first to make the scroll view visible.
  auto const* classroom_expand_button = GetClassroomExpandButtonView();
  ASSERT_TRUE(classroom_expand_button);
  LeftClickOn(classroom_expand_button);
  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const classroom_scroll_bar =
      GetClassroomScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(classroom_scroll_bar->GetVisible());
  const gfx::Point classroom_scroll_view_center =
      GetClassroomScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = classroom_scroll_bar->GetMaxPosition() -
                                 classroom_scroll_bar->GetMinPosition() + 10;

  // Scrolling downward to the bottom of the scroll view doesn't change expand
  // state.
  GenerateGestureScrollEvent(classroom_scroll_view_center, /*upward=*/false,
                             distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward when there is scrollable content doesn't change expand
  // state.
  GenerateGestureScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                             distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward at the top of the scroll view changes expand state.
  GenerateGestureScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                             distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest,
       MouseWheelScrollingUpFromTheTopOfClassroomExpandsTasks) {
  // Expand classroom first to make the scroll view visible.
  auto const* classroom_expand_button = GetClassroomExpandButtonView();
  ASSERT_TRUE(classroom_expand_button);
  LeftClickOn(classroom_expand_button);
  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const classroom_scroll_bar =
      GetClassroomScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(classroom_scroll_bar->GetVisible());
  const gfx::Point classroom_scroll_view_center =
      GetClassroomScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = classroom_scroll_bar->GetMaxPosition() -
                                 classroom_scroll_bar->GetMinPosition() + 10;

  // Scrolling downward to the bottom of the scroll view doesn't change expand
  // state.
  GetEventGenerator()->MoveMouseTo(classroom_scroll_view_center);
  GetEventGenerator()->MoveMouseWheel(0, -distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward when there is scrollable content doesn't change expand
  // state.
  GetEventGenerator()->MoveMouseWheel(0, distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Right after hitting the top of the scroll view, scrolling upward at the top
  // of the scroll view doesn't change expand state.
  GetEventGenerator()->MoveMouseWheel(0, distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // After the mouse wheel timer is fired, scrolling upward at the top of the
  // scroll view changes expand state.
  views::AsViewClass<GlanceablesContentsScrollView>(GetClassroomScrollView())
      ->FireMouseWheelTimerForTest();
  GetEventGenerator()->MoveMouseWheel(0, distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest, ScrollLockAfterOverscroll) {
  // Increase the number of tasks and assignments to ensure the scroll contents
  // overflow.
  classroom_client()->SetAssignmentsCount(10);
  PopulateTasks(10);

  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the scroll view is scrollable.
  auto* const tasks_scroll_bar = GetTasksScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(tasks_scroll_bar->GetVisible());
  const gfx::Point tasks_scroll_view_center =
      GetTasksScrollView()->GetBoundsInScreen().CenterPoint();

  // Set the distance that we want to scroll to the amount that is greater than
  // the scrollable length of the scroll view.
  const int distance_to_scroll = tasks_scroll_bar->GetMaxPosition() -
                                 tasks_scroll_bar->GetMinPosition() + 30;

  // Expand Classroom by scrolling.
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  const int tasks_scroll_bar_end_pos = tasks_scroll_bar->GetPosition();
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the classroom scroll view stays at its min position after
  // overscroll.
  const auto* const classroom_scroll_bar =
      GetClassroomScrollView()->vertical_scroll_bar();
  EXPECT_TRUE(classroom_scroll_bar->GetVisible());
  EXPECT_EQ(classroom_scroll_bar->GetPosition(),
            classroom_scroll_bar->GetMinPosition());

  // After the scroll lock timer fires, the scroll view can scroll as usual.
  const gfx::Point classroom_scroll_view_center =
      GetClassroomScrollView()->GetBoundsInScreen().CenterPoint();
  views::AsViewClass<GlanceablesContentsScrollView>(GetClassroomScrollView())
      ->FireScrollLockTimerForTest();
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_GT(classroom_scroll_bar->GetPosition(),
            classroom_scroll_bar->GetMinPosition());

  // Expand Tasks by scrolling.
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                              distance_to_scroll);
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                              distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
  // Make sure the tasks scroll view stays at its max position after overscroll.
  EXPECT_EQ(tasks_scroll_bar->GetPosition(), tasks_scroll_bar_end_pos);

  // After the scroll lock timer fires, the scroll view can scroll as usual.
  views::AsViewClass<GlanceablesContentsScrollView>(GetTasksScrollView())
      ->FireScrollLockTimerForTest();
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_LT(classroom_scroll_bar->GetPosition(), tasks_scroll_bar_end_pos);
}

TEST_F(GlanceablesTasksAndClassroomTest,
       NonScrollableGlanceablesCanStillScrollToToggleExpand) {
  // Set a bigger display to fit classroom items.
  UpdateDisplay("1920x1080");

  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Make sure the tasks scroll view is not scrollable.
  auto* const tasks_scroll_bar = GetTasksScrollView()->vertical_scroll_bar();
  EXPECT_FALSE(tasks_scroll_bar->GetVisible());
  const gfx::Point tasks_scroll_view_center =
      GetTasksScrollView()->GetBoundsInScreen().CenterPoint();

  const int distance_to_scroll = 10;

  // Scrolling downward when tasks scroll view is not scrollable expands
  // Classroom.
  GenerateGestureScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                             distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Make sure the classroom scroll view is not scrollable.
  auto* const classroom_scroll_bar =
      GetClassroomScrollView()->vertical_scroll_bar();
  EXPECT_FALSE(classroom_scroll_bar->GetVisible());
  const gfx::Point classroom_scroll_view_center =
      GetClassroomScrollView()->GetBoundsInScreen().CenterPoint();

  // Scrolling upward when classroom scroll view is not scrollable expands
  // Tasks.
  GenerateGestureScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                             distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward when tasks scroll view is not scrollable expands
  // Classroom.
  GenerateTrackpadScrollEvent(tasks_scroll_view_center, /*upward=*/false,
                              distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward when classroom scroll view is not scrollable expands
  // Tasks.
  GenerateTrackpadScrollEvent(classroom_scroll_view_center, /*upward=*/true,
                              distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Scrolling downward when tasks scroll view is not scrollable expands
  // Classroom.
  GetEventGenerator()->MoveMouseTo(tasks_scroll_view_center);
  GetEventGenerator()->MoveMouseWheel(0, -distance_to_scroll);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Scrolling upward when classroom scroll view is not scrollable expands
  // Tasks.
  GetEventGenerator()->MoveMouseTo(classroom_scroll_view_center);
  GetEventGenerator()->MoveMouseWheel(0, distance_to_scroll);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
}

TEST_F(GlanceablesTasksAndClassroomTest,
       ExpandCollapseAnimationSmoothnessHistogram) {
  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  base::HistogramTester resize_animation_histograms;

  // Smoothness should be recorded.
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Tasks.Expand.AnimationSmoothness", 0);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Tasks.Collapse.AnimationSmoothness", 0);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.Expand.AnimationSmoothness", 0);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.Collapse.AnimationSmoothness",
      0);

  auto* const tasks_view = GetTasksView();
  auto* const classroom_view = GetClassroomView();

  auto* const tasks_expand_button = GetTasksExpandButtonView();
  ASSERT_TRUE(tasks_expand_button);
  LeftClickOn(tasks_expand_button);
  // Make sure both view animations ended.
  ResizeAnimationWaiter(tasks_view).Wait();
  ResizeAnimationWaiter(classroom_view).Wait();
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Tasks.Expand.AnimationSmoothness", 0);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Tasks.Collapse.AnimationSmoothness", 1);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.Expand.AnimationSmoothness", 1);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.Collapse.AnimationSmoothness",
      0);

  auto const* classroom_expand_button = GetClassroomExpandButtonView();
  ASSERT_TRUE(classroom_expand_button);
  LeftClickOn(classroom_expand_button);
  // Make sure both view animations ended.
  ResizeAnimationWaiter(tasks_view).Wait();
  ResizeAnimationWaiter(classroom_view).Wait();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Tasks.Expand.AnimationSmoothness", 1);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Tasks.Collapse.AnimationSmoothness", 1);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.Expand.AnimationSmoothness", 1);
  resize_animation_histograms.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.Collapse.AnimationSmoothness",
      1);
}

TEST_F(GlanceablesTasksAndClassroomTest,
       PrefsRememberWhatUsersExpandedLastTime) {
  // Tasks should be expanded by default.
  auto* tasks_view = GetTasksView();
  auto* classroom_view = GetClassroomView();
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  view()->GetWidget()->LayoutRootViewIfNecessary();

  // Expand classroom.
  auto const* classroom_expand_button = GetClassroomExpandButtonView();
  ASSERT_TRUE(classroom_expand_button);
  LeftClickOn(classroom_expand_button);
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Reopen the glanceables again.
  ReopenGlanceables();
  tasks_view = GetTasksView();
  classroom_view = GetClassroomView();

  // Classroom should be expanded as this is expanded when the glanceables was
  // closed last time.
  EXPECT_FALSE(tasks_view->IsExpanded());
  EXPECT_TRUE(classroom_view->IsExpanded());

  // Expand Tasks.
  auto const* tasks_expand_button = GetTasksExpandButtonView();
  LeftClickOn(tasks_expand_button);
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());

  // Reopen the glanceables again.
  ReopenGlanceables();
  tasks_view = GetTasksView();
  classroom_view = GetClassroomView();

  // Tasks should be expanded as this is expanded when the glanceables was
  // closed last time.
  EXPECT_TRUE(tasks_view->IsExpanded());
  EXPECT_FALSE(classroom_view->IsExpanded());
}

}  // namespace ash