chromium/ash/glanceables/classroom/glanceables_classroom_student_view_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/glanceables/classroom/glanceables_classroom_student_view.h"

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

#include "ash/constants/ash_features.h"
#include "ash/glanceables/classroom/glanceables_classroom_client.h"
#include "ash/glanceables/classroom/glanceables_classroom_types.h"
#include "ash/glanceables/common/glanceables_list_footer_view.h"
#include "ash/glanceables/common/glanceables_view_id.h"
#include "ash/glanceables/common/test/glanceables_test_new_window_delegate.h"
#include "ash/glanceables/glanceables_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/combobox.h"
#include "ash/style/counter_expand_button.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/types/cxx23_to_underlying.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"

namespace ash {
namespace {

using ::testing::_;

class TestClient : public GlanceablesClassroomClient {
 public:
  bool IsDisabledByAdmin() const override { return false; }

  MOCK_METHOD(void,
              IsStudentRoleActive,
              (GlanceablesClassroomClient::IsRoleEnabledCallback),
              (override));

  MOCK_METHOD(void,
              GetCompletedStudentAssignments,
              (GlanceablesClassroomClient::GetAssignmentsCallback),
              (override));
  MOCK_METHOD(void,
              GetStudentAssignmentsWithApproachingDueDate,
              (GlanceablesClassroomClient::GetAssignmentsCallback),
              (override));
  MOCK_METHOD(void,
              GetStudentAssignmentsWithMissedDueDate,
              (GlanceablesClassroomClient::GetAssignmentsCallback),
              (override));
  MOCK_METHOD(void,
              GetStudentAssignmentsWithoutDueDate,
              (GlanceablesClassroomClient::GetAssignmentsCallback),
              (override));

  MOCK_METHOD(void, OnGlanceablesBubbleClosed, (), (override));
};

std::vector<std::unique_ptr<GlanceablesClassroomAssignment>> CreateAssignments(
    int count) {
  std::vector<std::unique_ptr<GlanceablesClassroomAssignment>> assignments;
  for (int i = 0; i < count; ++i) {
    assignments.push_back(std::make_unique<GlanceablesClassroomAssignment>(
        "Course title", base::StringPrintf("Course work title %d", i + 1),
        GURL(base::StringPrintf("https://classroom.google.com/test-link-%d",
                                i + 1)),
        std::nullopt, base::Time(), std::nullopt));
  }
  return assignments;
}

}  // namespace

class GlanceablesClassroomStudentViewTest : public AshTestBase {
 public:
  GlanceablesClassroomStudentViewTest() {
    feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {features::kGlanceablesTimeManagementClassroomStudentView},
        /*disabled_features=*/{});
  }

  void SetUp() override {
    AshTestBase::SetUp();
    SimulateUserLogin(account_id_);
    Shell::Get()->glanceables_controller()->UpdateClientsRegistration(
        account_id_, GlanceablesController::ClientsRegistration{
                         .classroom_client = &classroom_client_});
    ASSERT_TRUE(Shell::Get()->glanceables_controller()->GetClassroomClient());

    // `view_` gets student assignments with approaching due date during
    // initialization.
    EXPECT_CALL(classroom_client_,
                GetStudentAssignmentsWithApproachingDueDate(_))
        .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
          std::move(cb).Run(/*success=*/true, CreateAssignments(1));
        });

    widget_ = CreateFramelessTestWidget();
    widget_->SetFullscreen(true);

    view_ = widget_->SetContentsView(
        std::make_unique<GlanceablesClassroomStudentView>());
  }

  const views::View* GetHeaderIcon() const {
    return views::AsViewClass<views::View>(
        view_->GetViewByID(base::to_underlying(
            GlanceablesViewId::kTimeManagementBubbleHeaderIcon)));
  }

  Combobox* GetComboBoxView() {
    return views::AsViewClass<Combobox>(view_->GetViewByID(
        base::to_underlying(GlanceablesViewId::kTimeManagementBubbleComboBox)));
  }

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

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

  const views::View* GetListContainerView() const {
    return views::AsViewClass<views::View>(
        view_->GetViewByID(base::to_underlying(
            GlanceablesViewId::kTimeManagementBubbleListContainer)));
  }

  const views::View* GetEmptyListLabel() const {
    return views::AsViewClass<views::View>(
        view_->GetViewByID(base::to_underlying(
            GlanceablesViewId::kClassroomBubbleEmptyListLabel)));
  }

  views::View* GetListFooter() const {
    return views::AsViewClass<views::View>(
        view_->GetViewByID(base::to_underlying(
            GlanceablesViewId::kTimeManagementBubbleListFooter)));
  }

  const views::Label* GetListFooterLabel() const {
    return views::AsViewClass<views::Label>(GetListFooter()->GetViewByID(
        base::to_underlying(GlanceablesViewId::kListFooterTitleLabel)));
  }

  const views::LabelButton* GetListFooterSeeAllButton() const {
    return views::AsViewClass<views::LabelButton>(GetListFooter()->GetViewByID(
        base::to_underlying(GlanceablesViewId::kListFooterSeeAllButton)));
  }

  const views::ProgressBar* GetProgressBar() const {
    return views::AsViewClass<views::ProgressBar>(view_->GetViewByID(
        base::to_underlying(GlanceablesViewId::kProgressBar)));
  }

  const ErrorMessageToast* GetErrorMessage() const {
    return views::AsViewClass<ErrorMessageToast>(
        view_->GetViewByID(base::to_underlying(
            GlanceablesViewId::kTimeManagementErrorMessageToast)));
  }

  const views::LabelButton* GetMessageErrorDismissButton() const {
    return GetErrorMessage()->GetButtonForTest();
  }

  int GetLastSelectedAssignmentsListPrefValue() const {
    return Shell::Get()
        ->session_controller()
        ->GetActivePrefService()
        ->GetInteger(
            "ash.glanceables.classroom.student.last_selected_assignments_list");
  }

 protected:
  testing::StrictMock<TestClient> classroom_client_;
  std::unique_ptr<views::Widget> widget_;
  raw_ptr<GlanceablesClassroomStudentView> view_;
  const GlanceablesTestNewWindowDelegate new_window_delegate_;

 private:
  base::test::ScopedFeatureList feature_list_;
  AccountId account_id_ = AccountId::FromUserEmail("[email protected]");
};

TEST_F(GlanceablesClassroomStudentViewTest, Basics) {
  // Check that `GlanceablesClassroomStudentView` by itself doesn't have a
  // background.
  EXPECT_FALSE(view_->GetBackground());

  // Check that the expand button is not visible when
  // `GlanceablesClassroomStudentView` is created alone.
  auto* expand_button = view_->GetViewByID(base::to_underlying(
      GlanceablesViewId::kTimeManagementBubbleExpandButton));
  EXPECT_TRUE(expand_button);
  EXPECT_FALSE(expand_button->GetVisible());
}

TEST_F(GlanceablesClassroomStudentViewTest, RendersComboBoxView) {
  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithoutDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, {});
      });
  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithMissedDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, {});
      });
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, {});
      });
  Combobox* combobox_view = GetComboBoxView();
  ASSERT_TRUE(combobox_view);
  combobox_view->RequestFocus();
  ASSERT_TRUE(combobox_view->GetSelectedIndex());
  EXPECT_EQ(0u, *combobox_view->GetSelectedIndex());
  EXPECT_EQ(u"Due soon", combobox_view->GetTextForRow(0u));
  EXPECT_EQ(0, GetLastSelectedAssignmentsListPrefValue());

  PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
  ASSERT_TRUE(combobox_view->GetSelectedIndex());
  EXPECT_EQ(1u, *combobox_view->GetSelectedIndex());
  EXPECT_EQ(u"No due date", combobox_view->GetTextForRow(1u));
  EXPECT_EQ(1, GetLastSelectedAssignmentsListPrefValue());

  PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
  ASSERT_TRUE(combobox_view->GetSelectedIndex());
  EXPECT_EQ(2u, *combobox_view->GetSelectedIndex());
  EXPECT_EQ(u"Missing", combobox_view->GetTextForRow(2u));
  EXPECT_EQ(2, GetLastSelectedAssignmentsListPrefValue());

  PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
  ASSERT_TRUE(combobox_view->GetSelectedIndex());
  EXPECT_EQ(3u, *combobox_view->GetSelectedIndex());
  EXPECT_EQ(u"Done", combobox_view->GetTextForRow(3u));
  EXPECT_EQ(3, GetLastSelectedAssignmentsListPrefValue());

  PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
  ASSERT_TRUE(combobox_view->GetSelectedIndex());
  EXPECT_EQ(3u, *combobox_view->GetSelectedIndex());
}

TEST_F(GlanceablesClassroomStudentViewTest,
       ScrollViewResetPositionAfterSwitchingLists) {
  auto* scroll_bar = GetScrollView()->vertical_scroll_bar();

  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithoutDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(101));
      });
  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithMissedDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, {CreateAssignments(101)});
      });

  GetComboBoxView()->SelectMenuItemForTest(1);

  EXPECT_EQ(scroll_bar->GetPosition(), scroll_bar->GetMinPosition());
  ASSERT_TRUE(scroll_bar->GetVisible());
  scroll_bar->ScrollByAmount(views::ScrollBar::ScrollAmount::kEnd);
  EXPECT_GT(scroll_bar->GetPosition(), scroll_bar->GetMinPosition());

  GetComboBoxView()->SelectMenuItemForTest(2);
  EXPECT_EQ(scroll_bar->GetPosition(), scroll_bar->GetMinPosition());
}

TEST_F(GlanceablesClassroomStudentViewTest, RecordShowTimeHistogramOnClose) {
  base::HistogramTester histogram_tester;
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.TotalShowTime", 0);
  view_ = nullptr;
  widget_.reset();
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.TotalShowTime", 1);
}

TEST_F(GlanceablesClassroomStudentViewTest,
       ReadsInitialComboBoxViewValueFromPrefs) {
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .Times(2)
      .WillRepeatedly(
          [](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
            std::move(cb).Run(/*success=*/true, {});
          });

  ASSERT_TRUE(GetComboBoxView());

  // The first menu item is selected initially.
  EXPECT_EQ(GetLastSelectedAssignmentsListPrefValue(), 0);
  EXPECT_EQ(GetComboBoxView()->GetSelectedIndex(), 0u);

  // Update selection in the `combobox_view`, this should update prefs.
  GetComboBoxView()->SelectMenuItemForTest(3u);
  EXPECT_EQ(GetLastSelectedAssignmentsListPrefValue(), 3);
  EXPECT_EQ(GetComboBoxView()->GetSelectedIndex(), 3u);

  // Swap `widget_`'s content. Verify that the new `view_` contains a combobox
  // with the correct initial value.
  widget_->GetRootView()->RemoveChildViewT(std::exchange(view_, nullptr));
  view_ = widget_->SetContentsView(
      std::make_unique<GlanceablesClassroomStudentView>());
  ASSERT_TRUE(GetComboBoxView());
  EXPECT_EQ(GetLastSelectedAssignmentsListPrefValue(), 3);
  EXPECT_EQ(GetComboBoxView()->GetSelectedIndex(), 3u);
}

TEST_F(GlanceablesClassroomStudentViewTest,
       CallsClassroomClientAfterChangingActiveList) {
  base::UserActionTester user_actions;
  base::HistogramTester histogram_tester;
  ASSERT_TRUE(GetComboBoxView());
  ASSERT_TRUE(GetListFooterSeeAllButton());
  histogram_tester.ExpectUniqueSample(
      "Ash.Glanceables.Classroom.Student.ListSelected", 0,
      /*expected_bucket_count=*/0);

  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithoutDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(101));
      });

  GetComboBoxView()->SelectMenuItemForTest(1);
  // Trigger layout after receiving new items.
  widget_->LayoutRootViewIfNecessary();
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.Classroom.Student.ListSelected", 1,
      /*expected_count=*/1);
  EXPECT_TRUE(GetListFooter()->GetVisible());
  GetListFooter()->ScrollViewToVisible();
  LeftClickOn(GetListFooterSeeAllButton());
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/u/0/a/not-turned-in/all");

  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithMissedDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(101));
      });
  GetComboBoxView()->SelectMenuItemForTest(2);
  // Trigger layout after receiving new items.
  widget_->LayoutRootViewIfNecessary();
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.Classroom.Student.ListSelected", 2,
      /*expected_count=*/1);
  GetListFooter()->ScrollViewToVisible();
  LeftClickOn(GetListFooterSeeAllButton());
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/u/0/a/missing/all");

  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(101));
      });
  GetComboBoxView()->SelectMenuItemForTest(3);
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.Classroom.Student.ListSelected", 3,
      /*expected_count=*/1);
  // Trigger layout after receiving new items.
  widget_->LayoutRootViewIfNecessary();
  GetListFooter()->ScrollViewToVisible();
  LeftClickOn(GetListFooterSeeAllButton());
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/u/0/a/turned-in/all");

  // Switch from the final assignment list back to no due date list.
  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithoutDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(3));
      });
  GetComboBoxView()->SelectMenuItemForTest(1);
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.Classroom.Student.ListSelected", 1,
      /*expected_count=*/2);

  EXPECT_EQ(3,
            user_actions.GetActionCount("Glanceables_Classroom_SeeAllPressed"));
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.Classroom.Student.AssignmentListShownTime.DefaultList."
      "Assigned",
      1);
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.Classroom.Student.AssignmentListShownTime.ChangedList."
      "NoDueDate",
      1);
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.Classroom.Student.AssignmentListShownTime.ChangedList."
      "Missing",
      1);
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.Classroom.Student.AssignmentListShownTime.ChangedList."
      "Done",
      1);
}

TEST_F(GlanceablesClassroomStudentViewTest, RendersListItems) {
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(101));
      });
  ASSERT_TRUE(GetComboBoxView());
  ASSERT_TRUE(GetListContainerView());

  GetComboBoxView()->SelectMenuItemForTest(3);
  EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), 100u);
  EXPECT_EQ(GetListContainerView()->children().size(), 100u);

  EXPECT_TRUE(GetListFooter()->GetVisible());
  GetListFooter()->ScrollViewToVisible();
  ASSERT_TRUE(GetListFooterLabel());
  EXPECT_EQ(GetListFooterLabel()->GetText(),
            u"See all assignments in Google Classroom");
}

TEST_F(GlanceablesClassroomStudentViewTest, RendersEmptyListLabel) {
  ASSERT_TRUE(GetComboBoxView());
  ASSERT_TRUE(GetListContainerView());
  EXPECT_FALSE(GetEmptyListLabel()->GetVisible());
  EXPECT_FALSE(GetListFooter()->GetVisible());
  EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), 1u);
  EXPECT_EQ(GetListContainerView()->children().size(), 1u);

  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithoutDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, {});
      });
  GetComboBoxView()->SelectMenuItemForTest(1);
  EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), 0u);
  EXPECT_EQ(GetListContainerView()->children().size(), 0u);

  // The empty list label should be shown, and the footer hidden.
  EXPECT_FALSE(GetListFooter()->GetVisible());
  EXPECT_TRUE(GetEmptyListLabel()->GetVisible());

  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithMissedDueDate(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, {});
      });
  GetComboBoxView()->SelectMenuItemForTest(2);
  EXPECT_EQ(GetCounterExpandButton()->counter_for_test(), 0u);
  EXPECT_EQ(GetListContainerView()->children().size(), 0u);

  // The empty list label should be shown, and the footer hidden.
  EXPECT_FALSE(GetListFooter()->GetVisible());
  EXPECT_TRUE(GetEmptyListLabel()->GetVisible());
}

TEST_F(GlanceablesClassroomStudentViewTest, OpensClassroomUrlForListItem) {
  base::UserActionTester user_actions;
  base::HistogramTester histogram_tester;
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(1));
      });
  ASSERT_TRUE(GetComboBoxView());
  GetComboBoxView()->SelectMenuItemForTest(3);

  // Trigger layout for `GetListContainerView()` after receiving new items.
  widget_->LayoutRootViewIfNecessary();

  ASSERT_TRUE(GetListContainerView());
  ASSERT_EQ(GetListContainerView()->children().size(), 1u);

  LeftClickOn(GetListContainerView()->children().at(0));
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/test-link-1");

  EXPECT_EQ(1, user_actions.GetActionCount(
                   "Glanceables_Classroom_AssignmentPressed"));
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 2);
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 0,
      /*expected_count=*/1);
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 2,
      /*expected_count=*/1);
}

TEST_F(GlanceablesClassroomStudentViewTest, ShowsProgressBar) {
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([&](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        // Progress bar is visible before replying to pending request.
        EXPECT_TRUE(GetProgressBar()->GetVisible());

        std::move(cb).Run(/*success=*/true, {});

        // Progress bar is hidden after replying to pending request.
        EXPECT_FALSE(GetProgressBar()->GetVisible());
      });

  ASSERT_TRUE(GetProgressBar());
  ASSERT_TRUE(GetComboBoxView());
  GetComboBoxView()->SelectMenuItemForTest(3);
}

TEST_F(GlanceablesClassroomStudentViewTest, ClickHeaderIconButton) {
  base::UserActionTester user_actions;
  base::HistogramTester histogram_tester;

  LeftClickOn(GetHeaderIcon());
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/u/0/h");

  EXPECT_EQ(1, user_actions.GetActionCount(
                   "Glanceables_Classroom_HeaderIconPressed"));
  histogram_tester.ExpectUniqueSample(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 1,
      /*expected_bucket_count=*/1);
}

TEST_F(GlanceablesClassroomStudentViewTest, ClickItemViewUserAction) {
  base::UserActionTester user_actions;
  base::HistogramTester histogram_tester;

  LeftClickOn(GetListContainerView()->children().at(0));
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/test-link-1");

  EXPECT_EQ(1, user_actions.GetActionCount(
                   "Glanceables_Classroom_AssignmentPressed_DefaultList"));

  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(2));
      });
  ASSERT_TRUE(GetComboBoxView());
  GetComboBoxView()->SelectMenuItemForTest(3);

  LeftClickOn(GetListContainerView()->children().at(1));
  EXPECT_EQ(new_window_delegate_.GetLastOpenedUrl(),
            "https://classroom.google.com/test-link-2");

  EXPECT_EQ(2, user_actions.GetActionCount(
                   "Glanceables_Classroom_AssignmentPressed"));
  histogram_tester.ExpectUniqueSample(
      "Ash.Glanceables.Classroom.Student.ListSelected", 3,
      /*expected_bucket_count=*/1);
  histogram_tester.ExpectTotalCount(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 3);
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 0,
      /*expected_count=*/1);
  histogram_tester.ExpectBucketCount(
      "Ash.Glanceables.TimeManagement.Classroom.UserAction", 2,
      /*expected_count=*/2);
}

TEST_F(GlanceablesClassroomStudentViewTest, ShowErrorMessageBubble) {
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([&](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        // Error message is not initialized before replying to pending request.
        EXPECT_FALSE(GetErrorMessage());

        std::move(cb).Run(/*success=*/false, {});

        // Error message is created and visible after receiving unsuccessful
        // response.
        ASSERT_TRUE(GetErrorMessage());
        EXPECT_TRUE(GetErrorMessage()->GetVisible());
      });

  ASSERT_FALSE(GetErrorMessage());
  ASSERT_TRUE(GetComboBoxView());
  GetComboBoxView()->SelectMenuItemForTest(3);
}

TEST_F(GlanceablesClassroomStudentViewTest, DismissErrorMessageBubble) {
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([&](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/false, {});
        ASSERT_TRUE(GetErrorMessage()->GetVisible());
      });

  ASSERT_FALSE(GetErrorMessage());
  ASSERT_TRUE(GetComboBoxView());
  GetComboBoxView()->SelectMenuItemForTest(3);

  // Trigger layout after receiving unsuccessful response.
  widget_->LayoutRootViewIfNecessary();
  ASSERT_TRUE(GetErrorMessage());

  LeftClickOn(GetMessageErrorDismissButton());
  EXPECT_FALSE(GetErrorMessage());
}

TEST_F(GlanceablesClassroomStudentViewTest,
       DismissErrorMessageBubbleAfterSuccess) {
  EXPECT_CALL(classroom_client_, GetCompletedStudentAssignments(_))
      .WillOnce([&](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/false, {});
      });

  ASSERT_FALSE(GetErrorMessage());
  ASSERT_TRUE(GetComboBoxView());
  GetComboBoxView()->SelectMenuItemForTest(3);

  // Trigger layout after receiving unsuccessful response.
  widget_->LayoutRootViewIfNecessary();
  ASSERT_TRUE(GetErrorMessage());

  EXPECT_CALL(classroom_client_, GetStudentAssignmentsWithoutDueDate(_))
      .WillOnce([&](GlanceablesClassroomClient::GetAssignmentsCallback cb) {
        std::move(cb).Run(/*success=*/true, CreateAssignments(1));
      });
  GetComboBoxView()->SelectMenuItemForTest(1);

  // Trigger layout after receiving successful response.
  widget_->LayoutRootViewIfNecessary();
  EXPECT_FALSE(GetErrorMessage());
}

}  // namespace ash