chromium/ash/system/notification_center/views/notification_center_view_unittest.cc

// Copyright 2022 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/notification_center/views/notification_center_view.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/focus_cycler.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/notification_center/ash_message_center_lock_screen_controller.h"
#include "ash/system/notification_center/message_center_constants.h"
#include "ash/system/notification_center/notification_center_test_api.h"
#include "ash/system/notification_center/views/notification_list_view.h"
#include "ash/system/notification_center/stacked_notification_bar.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_model.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "components/prefs/pref_service.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/message_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/test/views_test_utils.h"
#include "url/gurl.h"

using message_center::MessageCenter;
using message_center::MessageView;
using message_center::Notification;

namespace ash {

class NotificationCenterViewTest : public AshTestBase,
                                   public views::ViewObserver,
                                   public testing::WithParamInterface<
                                       /*enable_ongoing_processes=*/bool> {
 public:
  NotificationCenterViewTest() {
    scoped_feature_list_.InitWithFeatureState(features::kOngoingProcesses,
                                              AreOngoingProcessesEnabled());
  }

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

    test_api_ = std::make_unique<NotificationCenterTestApi>();
  }

  void TearDown() override {
    base::RunLoop().RunUntilIdle();
    AshTestBase::TearDown();
  }

  // views::ViewObserver:
  void OnViewPreferredSizeChanged(views::View* view) override {
    if (view->GetPreferredSize() == view->size()) {
      return;
    }
    view->SetBoundsRect(view->GetVisible() ? gfx::Rect(view->GetPreferredSize())
                                           : gfx::Rect());
    views::test::RunScheduledLayout(view);
    ++size_changed_count_;
  }

  bool AreOngoingProcessesEnabled() const { return GetParam(); }

 protected:
  // Adds more than enough notifications to make the message center scrollable.
  std::vector<std::string> AddManyNotifications() {
    std::vector<std::string> ids;
    for (int i = 0; i < 10; ++i) {
      ids.push_back(test_api()->AddNotification());
    }
    return ids;
  }

  void AnimateNotificationListToValue(float value) {
    GetNotificationListView()->animation_->SetCurrentValue(value);
    GetNotificationListView()->AnimationProgressed(
        GetNotificationListView()->animation_.get());
  }

  void AnimateNotificationListToMiddle() {
    AnimateNotificationListToValue(0.5);
  }

  void AnimateNotificationListToEnd() {
    FinishNotificationListSlideOutAnimations();
    GetNotificationListView()->animation_->End();
  }

  void AnimateNotificationListUntilIdle() {
    while (GetNotificationListView()->animation_->is_animating()) {
      GetNotificationListView()->animation_->End();
    }
  }

  void FinishNotificationListSlideOutAnimations() {
    base::RunLoop().RunUntilIdle();
  }

  gfx::Rect GetMessageViewVisibleBounds(size_t index) {
    gfx::Rect bounds = GetNotificationListView()->children()[index]->bounds();
    bounds -= GetScroller()->GetVisibleRect().OffsetFromOrigin();
    bounds += GetScroller()->bounds().OffsetFromOrigin();
    return bounds;
  }

  NotificationListView* GetNotificationListView() {
    return notification_center_view()->notification_list_view_;
  }

  views::ScrollView* GetScroller() {
    return notification_center_view()->scroller_;
  }

  views::ScrollBar* GetScrollBar() {
    return notification_center_view()->scroll_bar_;
  }

  views::View* GetScrollerContents() {
    return notification_center_view()->scroller_->contents();
  }

  StackedNotificationBar* GetNotificationBar() {
    return notification_center_view()->notification_bar_;
  }

  views::View* GetNotificationBarIconsContainer() {
    return notification_center_view()
        ->notification_bar_->notification_icons_container_;
  }

  views::View* GetNotificationBarLabel() {
    return notification_center_view()->notification_bar_->count_label_;
  }

  views::View* GetNotificationBarClearAllButton() {
    return notification_center_view()->notification_bar_->clear_all_button_;
  }

  int total_notification_count() {
    return GetNotificationBar()->total_notification_count_;
  }

  int pinned_notification_count() {
    return GetNotificationBar()->pinned_notification_count_;
  }

  int unpinned_notification_count() {
    return GetNotificationBar()->total_notification_count_ -
           GetNotificationBar()->pinned_notification_count_;
  }

  int stacked_notification_count() {
    return GetNotificationBar()->stacked_notification_count_;
  }

  message_center::MessageView* FocusNotificationView(const std::string& id) {
    auto* focus_manager = notification_center_view()->GetFocusManager();
    if (!focus_manager) {
      return nullptr;
    }

    auto* focused_message_view = test_api()->GetNotificationViewForId(id);
    focus_manager->SetFocusedView(focused_message_view);
    return focused_message_view;
  }

  void FocusClearAllButton() {
    auto* widget = GetNotificationBarClearAllButton()->GetWidget();
    widget->widget_delegate()->SetCanActivate(true);
    Shell::Get()->focus_cycler()->FocusWidget(widget);
    GetNotificationBarClearAllButton()->RequestFocus();
  }

  void RelayoutMessageCenterViewForTest() {
    // Outside of tests, any changes to bubble's size as well as scrolling
    // through notification list will trigger TrayBubbleView's BoxLayout to
    // relayout, and then this view will relayout. In test, we don't have
    // TrayBubbleView as the parent, so we need to ensure layout is executed in
    // some circumstances.
    views::test::RunScheduledLayout(test_api()->GetNotificationCenterView());
  }

  void UpdateNotificationBarForTest() {
    // TODO(crbug/1357232): Refactor so this code mirrors production better.
    // Outside of tests, the notification bar is updated with a call to
    // NotificationCenterBubble::UpdatePosition(), but this function is not
    // triggered when adding notifications in tests.
    test_api_->GetNotificationCenterView()->UpdateNotificationBar();
  }

  virtual NotificationCenterView* notification_center_view() {
    return test_api_->GetNotificationCenterView();
  }

  int size_changed_count() const { return size_changed_count_; }

  NotificationCenterTestApi* test_api() { return test_api_.get(); }

 private:
  int size_changed_count_ = 0;

  std::unique_ptr<NotificationCenterTestApi> test_api_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All,
                         NotificationCenterViewTest,
                         /*enable_ongoing_processes=*/testing::Bool());

TEST_P(NotificationCenterViewTest, ContentsRelayout) {
  std::vector<std::string> ids = AddManyNotifications();
  test_api()->ToggleBubble();
  EXPECT_TRUE(notification_center_view()->GetVisible());
  // MessageCenterView is maxed out.
  EXPECT_GT(GetNotificationListView()->bounds().height(),
            notification_center_view()->bounds().height());
  const int previous_contents_height = GetScrollerContents()->height();
  const int previous_list_height = GetNotificationListView()->height();
  EXPECT_EQ(previous_contents_height, previous_list_height);

  test_api()->RemoveNotification(ids.back());
  AnimateNotificationListToEnd();
  RelayoutMessageCenterViewForTest();

  EXPECT_TRUE(notification_center_view()->GetVisible());
  EXPECT_GT(previous_contents_height, GetScrollerContents()->height());
  EXPECT_GT(previous_list_height, GetNotificationListView()->height());
  EXPECT_EQ(GetScrollerContents()->height(),
            GetNotificationListView()->height());
}

TEST_P(NotificationCenterViewTest, VisibleWhenLocked) {
  // This test is only valid if the lock screen feature is enabled.
  // TODO(yoshiki): Clean up after the feature is launched crbug.com/913764.
  if (!features::IsLockScreenNotificationsEnabled()) {
    return;
  }

  // Enables the lock screen notification if the feature is disabled.
  PrefService* user_prefs =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  user_prefs->SetString(prefs::kMessageCenterLockScreenMode,
                        prefs::kMessageCenterLockScreenModeShow);

  ASSERT_TRUE(AshMessageCenterLockScreenController::IsEnabled());

  test_api()->AddNotification();
  test_api()->AddNotification();

  BlockUserSession(BLOCKED_BY_LOCK_SCREEN);
  test_api()->ToggleBubble();

  EXPECT_TRUE(notification_center_view()->GetVisible());
}

TEST_P(NotificationCenterViewTest, ClearAllPressed) {
  test_api()->AddNotification();
  test_api()->AddNotification();
  test_api()->ToggleBubble();
  EXPECT_TRUE(notification_center_view()->GetVisible());
  EXPECT_TRUE(GetNotificationBar()->GetVisible());

  // When Clear All button is pressed, all notifications are removed and the
  // view becomes invisible.
  notification_center_view()->ClearAllNotifications();
}

TEST_P(NotificationCenterViewTest, InitialPosition) {
  test_api()->AddNotification();
  test_api()->AddNotification();
  test_api()->ToggleBubble();
  EXPECT_TRUE(notification_center_view()->GetVisible());

  // MessageCenterView is not maxed out.
  EXPECT_LT(GetNotificationListView()->bounds().height(),
            notification_center_view()->bounds().height());
}

TEST_P(NotificationCenterViewTest, InitialPositionMaxOut) {
  AddManyNotifications();
  test_api()->ToggleBubble();
  EXPECT_TRUE(notification_center_view()->GetVisible());

  // MessageCenterView is maxed out.
  EXPECT_GT(GetNotificationListView()->bounds().height(),
            notification_center_view()->bounds().height());
}

// Tests basic layout of the StackingNotificationBar.
TEST_P(NotificationCenterViewTest, StackingCounterLabelLayout) {
  UpdateDisplay("800x500");

  AddManyNotifications();

  // MessageCenterView is maxed out.
  test_api()->ToggleBubble();

  EXPECT_GT(GetNotificationListView()->bounds().height(),
            notification_center_view()->bounds().height());

  EXPECT_TRUE(GetNotificationBar()->GetVisible());

  EXPECT_EQ(kMessageCenterPadding, GetScroller()->bounds().y());
  EXPECT_EQ(GetNotificationBar()->bounds().y(),
            GetScroller()->bounds().bottom());

  EXPECT_TRUE(GetNotificationBarLabel()->GetVisible());

  EXPECT_TRUE(GetNotificationBarClearAllButton()->GetVisible());
}

// Tests that the NotificationBarLabel is invisible when scrolled to the top.
TEST_P(NotificationCenterViewTest, StackingCounterLabelInvisible) {
  UpdateDisplay("800x500");

  AddManyNotifications();
  test_api()->ToggleBubble();

  // Scroll to the bottom, the counter label should be invisible.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(
      notification_center_view()->GetBoundsInScreen().CenterPoint());
  event_generator->MoveMouseWheel(0, -10000);

  EXPECT_FALSE(GetNotificationBarLabel()->GetVisible());
  // ClearAll label should always be visible.
  EXPECT_TRUE(GetNotificationBarClearAllButton()->GetVisible());
}

// Tests that the NotificationBarLabel is visible when there are enough excess
// notifications.
TEST_P(NotificationCenterViewTest, StackingCounterLabelVisible) {
  UpdateDisplay("800x500");

  AddManyNotifications();
  test_api()->ToggleBubble();

  EXPECT_TRUE(GetNotificationBarLabel()->GetVisible());
  // ClearAll label should always be visible.
  EXPECT_TRUE(GetNotificationBarClearAllButton()->GetVisible());
}

// Tests that the +n notifications label hides after being shown.
TEST_P(NotificationCenterViewTest, StackingCounterLabelHidesAfterShown) {
  UpdateDisplay("800x500");

  AddManyNotifications();
  test_api()->ToggleBubble();

  // Scroll to the bottom, making the counter label invisible.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(
      notification_center_view()->GetBoundsInScreen().CenterPoint());
  event_generator->MoveMouseWheel(0, -10000);

  EXPECT_FALSE(GetNotificationBarLabel()->GetVisible());

  // Scrolling past 5 notifications should make the counter label visible.
  const int scroll_amount = (GetMessageViewVisibleBounds(0).height() * 5) + 1;
  event_generator->MoveMouseWheel(0, scroll_amount);

  ASSERT_TRUE(GetNotificationBarLabel()->GetVisible());

  // Scrolling back to the bottom should make the
  // counter label invisible again.
  event_generator->MoveMouseWheel(0, -10000);

  EXPECT_FALSE(GetNotificationBarLabel()->GetVisible());
  // ClearAll label should always be visible.
  EXPECT_TRUE(GetNotificationBarClearAllButton()->GetVisible());
}

// Tests that there are never more than 3 stacked icons in the
// StackedNotificationBar. Also verifies that only one animation happens at a
// time (this prevents the user from over-scrolling and showing multiple
// animations when they scroll very quickly). Before, users could scroll fast
// and have a large amount of icons, instead of keeping it to 3.
TEST_P(NotificationCenterViewTest, StackingIconsNeverMoreThanThree) {
  for (int i = 0; i < 20; ++i) {
    test_api()->AddNotification();
  }
  test_api()->ToggleBubble();

  auto bottom_position = GetScrollBar()->bounds().bottom();
  GetScroller()->ScrollToPosition(GetScrollBar(), bottom_position);

  // Force animations to happen, so we can see if multiple animations trigger.
  ui::ScopedAnimationDurationScaleMode scoped_duration_modifier(
      ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
  // Scroll past 20 notifications, so we can scroll back up quickly.
  for (int i = 20; i >= 0; --i) {
    const int scroll_amount = (GetMessageViewVisibleBounds(0).height() * i) + 1;
    GetScroller()->ScrollToPosition(GetScrollBar(),
                                    bottom_position - scroll_amount);

    auto icons_container_children =
        GetNotificationBarIconsContainer()->children();
    int animating_count = 0;
    for (views::View* child : icons_container_children) {
      // Verify that no more than one icon is animating at any one time.
      if (child->layer()->GetAnimator()->is_animating()) {
        animating_count++;
      }
    }
    EXPECT_GE(1, animating_count);
    // Verify that no more than 3 icons are added to the bar at any one time,
    // regardless of how fast the user scrolls. This test scrolls faster than
    // the icons can animate away, and animating icons should be removed prior
    // to starting a new animation.
    EXPECT_GE(3u, icons_container_children.size());
  }
}

TEST_P(NotificationCenterViewTest, StackingCounterLabelRelaidOutOnScroll) {
  // Open the message center at the top of the notification list so the stacking
  // bar is hidden by default.
  std::string id = test_api()->AddNotification();
  int total_notifications = 30;
  for (int i = 0; i < total_notifications; ++i) {
    test_api()->AddNotification();
  }
  GetPrimaryUnifiedSystemTray()->model()->SetTargetNotification(id);

  test_api()->ToggleBubble();

  auto bottom_position =
      GetMessageViewVisibleBounds(total_notifications - 1).bottom();

  GetScroller()->ScrollToPosition(GetScrollBar(), bottom_position);

  EXPECT_FALSE(GetNotificationBarLabel()->GetVisible());

  // Scroll past 6 notifications so the count label becomes visible
  int scroll_amount = (GetMessageViewVisibleBounds(0).height() * 6) + 1;
  GetScroller()->ScrollToPosition(GetScrollBar(),
                                  bottom_position - scroll_amount);
  RelayoutMessageCenterViewForTest();
  EXPECT_TRUE(GetNotificationBarLabel()->GetVisible());
  int label_width = GetNotificationBarLabel()->bounds().width();
  EXPECT_GT(label_width, 0);

  // Scroll past 14 notifications so the label width must be expanded to
  // contain longer 2-digit label.
  scroll_amount = (GetMessageViewVisibleBounds(0).height() * 14) + 1;
  GetScroller()->ScrollToPosition(GetScrollBar(),
                                  bottom_position - scroll_amount);
  RelayoutMessageCenterViewForTest();
  EXPECT_GT(GetNotificationBarLabel()->bounds().width(), label_width);
}

TEST_P(NotificationCenterViewTest, FocusClearedAfterNotificationRemoval) {
  test_api()->AddNotification();
  auto id1 = test_api()->AddNotification();

  test_api()->ToggleBubble();

  // Focus the latest notification MessageView.
  auto* focused_message_view = FocusNotificationView(id1);
  ASSERT_TRUE(focused_message_view);
  EXPECT_EQ(id1, focused_message_view->notification_id());

  // Remove the notification and observe that the focus is cleared.
  test_api()->RemoveNotification(id1);
  AnimateNotificationListToEnd();
  EXPECT_FALSE(notification_center_view()->GetFocusManager()->GetFocusedView());
}

TEST_P(NotificationCenterViewTest, ClearAllButtonHeight) {
  std::string id0 = test_api()->AddNotification();
  std::string id1 = test_api()->AddNotification();
  test_api()->ToggleBubble();
  EXPECT_TRUE(notification_center_view()->GetVisible());
  EXPECT_TRUE(GetNotificationBar()->GetVisible());
  EXPECT_TRUE(GetNotificationBarClearAllButton()->GetVisible());

  // Get ClearAll Button height.
  const int previous_button_height =
      GetNotificationBarClearAllButton()->height();

  // Remove a notification.
  test_api()->RemoveNotification(id0);

  // ClearAll Button height should remain the same.
  EXPECT_EQ(previous_button_height,
            GetNotificationBarClearAllButton()->height());
}

// Tests that the "Clear all" button is not focusable when it is disabled.
TEST_P(NotificationCenterViewTest, ClearAllNotFocusableWhenDisabled) {
  // Add a pinned notification and toggle the bubble.
  test_api()->AddPinnedNotification();
  test_api()->ToggleBubble();

  // Verify that the "Clear all" button is visible but disabled.
  ASSERT_TRUE(GetNotificationBarClearAllButton()->GetVisible());
  ASSERT_FALSE(GetNotificationBarClearAllButton()->GetEnabled());

  // Attempt to focus the "Clear all" button.
  FocusClearAllButton();

  // Verify that the "Clear all" button did not receive focus.
  EXPECT_FALSE(GetNotificationBarClearAllButton()->HasFocus());
}

TEST_P(NotificationCenterViewTest, StackedNotificationCount) {
  // There should not be any stacked notifications in the message
  // center with just one notification added.
  test_api()->AddNotification();
  test_api()->ToggleBubble();
  EXPECT_TRUE(notification_center_view()->GetVisible());
  EXPECT_EQ(1, total_notification_count());
  EXPECT_EQ(0, stacked_notification_count());

  // There should be at least one stacked notification in the expanded message
  // center with many notifications added.
  AddManyNotifications();
  RelayoutMessageCenterViewForTest();
  UpdateNotificationBarForTest();
  EXPECT_EQ(11, total_notification_count());
  EXPECT_LT(0, stacked_notification_count());
}

// Test for notification swipe control visibility.
TEST_P(NotificationCenterViewTest, NotificationPartialSwipe) {
  auto id1 = test_api()->AddNotification();
  test_api()->ToggleBubble();
  auto* view = test_api()->GetNotificationViewForId(id1);

  int x_start = view->GetBoundsInScreen().x();
  GetEventGenerator()->GestureScrollSequence(
      view->GetBoundsInScreen().CenterPoint(),
      view->GetBoundsInScreen().right_center(), base::Milliseconds(1000), 1000);

  // The notification view should go back to it's original location after a
  // partial swipe when there is no settings button.
  EXPECT_EQ(x_start, view->GetBoundsInScreen().x());

  message_center::RichNotificationData optional_fields;
  optional_fields.settings_button_handler =
      message_center::SettingsButtonHandler::INLINE;
  auto id2 = test_api()->AddCustomNotification(
      u"title", u"message", ui::ImageModel(), std::u16string(), GURL(),
      message_center::NotifierId(), optional_fields);

  view = test_api()->GetNotificationViewForId(id2);

  x_start = view->GetBoundsInScreen().x();
  GetEventGenerator()->GestureScrollSequence(
      view->GetBoundsInScreen().CenterPoint(),
      view->GetBoundsInScreen().right_center(), base::Milliseconds(1000), 1000);

  // The notification view should be offset forwards from it's start position to
  // make space for the settings button at the end of a swipe.
  EXPECT_LT(x_start, view->GetBoundsInScreen().x());
}

}  // namespace ash