chromium/ash/system/notification_center/notification_center_bubble_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/notification_center_bubble.h"

#include <cstdint>
#include <string>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/notification_center/message_center_utils.h"
#include "ash/system/notification_center/notification_center_bubble.h"
#include "ash/system/notification_center/notification_center_test_api.h"
#include "ash/system/notification_center/views/ash_notification_view.h"
#include "ash/system/notification_center/views/notification_center_view.h"
#include "ash/system/notification_center/views/notification_list_view.h"
#include "ash/system/status_area_widget.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/message_center/views/message_popup_view.h"
#include "ui/message_center/views/message_view.h"

namespace ash {

class NotificationCenterBubbleTestBase : public AshTestBase {
 public:
  NotificationCenterBubbleTestBase(bool enable_ongoing_processes)
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
        enable_ongoing_processes_(enable_ongoing_processes) {
    scoped_feature_list_.InitWithFeatureState(features::kOngoingProcesses,
                                              AreOngoingProcessesEnabled());
  }

  void SetUp() override {
    AshTestBase::SetUp();
    test_api_ = std::make_unique<NotificationCenterTestApi>();
  }

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

  bool AreOngoingProcessesEnabled() const { return enable_ongoing_processes_; }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<NotificationCenterTestApi> test_api_;
  bool enable_ongoing_processes_ = false;
};

class NotificationCenterBubbleTest : public NotificationCenterBubbleTestBase,
                                     public testing::WithParamInterface<
                                         /*enable_ongoing_processes=*/bool> {
 public:
  NotificationCenterBubbleTest()
      : NotificationCenterBubbleTestBase(
            /*enable_ongoing_processes=*/GetParam()) {}
};

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

// Tests that the notification bubble does not get cut off by the top of the
// screen on the launcher homescreen in tablet mode; see b/278471988.
TEST_P(NotificationCenterBubbleTest,
       TopOfBubbleConstrainedByTopOfDisplayInTabletModeHomescreen) {
  // Set the display to some known size.
  UpdateDisplay("1200x800");

  // Switch to tablet mode and verify we're on the launcher homescreen.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  ASSERT_EQ(ShelfBackgroundType::kHomeLauncher,
            GetPrimaryShelf()->GetBackgroundType());

  // Add a large number of notifications to overflow the scroll view in the
  // notification center.
  for (int i = 0; i < 100; i++) {
    test_api()->AddNotification();
  }

  // Show the notification center bubble.
  test_api()->ToggleBubble();

  // Verify that the top of the notification center bubble window is not beyond
  // the top of the display.
  EXPECT_GE(
      test_api()->GetBubble()->GetBubbleWidget()->GetWindowBoundsInScreen().y(),
      0);
}

TEST_P(NotificationCenterBubbleTest, BubbleHeightConstrainedByDisplay) {
  const int display_height = 800;
  UpdateDisplay("1200x" + base::NumberToString(display_height));

  // Add a large number of notifications to overflow the scroll view in the
  // notification center.
  for (int i = 0; i < 100; i++) {
    test_api()->AddNotification();
  }

  // Show notification center bubble.
  test_api()->ToggleBubble();

  // The height of the notification center should not exceed the
  // display height.
  EXPECT_LT(test_api()->GetNotificationCenterView()->bounds().height(),
            display_height);
}

TEST_P(NotificationCenterBubbleTest, BubbleHeightUpdatedByDisplaySizeChange) {
  UpdateDisplay("800x600");

  // Add a large number of notifications to overflow the scroll view in the
  // notification center.
  for (int i = 0; i < 100; i++) {
    test_api()->AddNotification();
  }

  // Show notification center bubble.
  test_api()->ToggleBubble();

  gfx::Rect previous_bounds = test_api()->GetNotificationCenterView()->bounds();

  UpdateDisplay("1600x800");

  gfx::Rect current_bounds = test_api()->GetNotificationCenterView()->bounds();

  // The height of the notification center should increase as the display height
  // has increased. However, the width should stay constant.
  EXPECT_GT(current_bounds.height(), previous_bounds.height());
  EXPECT_EQ(current_bounds.width(), previous_bounds.width());
}

TEST_P(NotificationCenterBubbleTest, BubbleHeightUpdatedByDisplayRotation) {
  const int display_width = 1000;
  const int display_height = 600;
  UpdateDisplay(base::NumberToString(display_width) + "x" +
                base::NumberToString(display_height));

  // Add a large number of notifications to overflow the scroll view in the
  // notification center.
  for (int i = 0; i < 100; i++) {
    test_api()->AddNotification();
  }

  // Show notification center bubble.
  test_api()->ToggleBubble();

  // Rotate the display to portrait mode.
  auto* display_manager = Shell::Get()->display_manager();
  display::Display display = GetPrimaryDisplay();
  display_manager->SetDisplayRotation(display.id(), display::Display::ROTATE_90,
                                      display::Display::RotationSource::ACTIVE);

  auto* notification_center_view = test_api()->GetNotificationCenterView();

  // In portrait mode the notification center's height should be constrained by
  // the original `display_width`.
  EXPECT_GT(notification_center_view->bounds().height(), display_height);
  EXPECT_LT(notification_center_view->bounds().height(), display_width);

  // Rotate back to landscape mode.
  display_manager->SetDisplayRotation(display.id(), display::Display::ROTATE_0,
                                      display::Display::RotationSource::ACTIVE);

  // In landspace mode the height constraint should be back to `display_height`.
  EXPECT_LT(notification_center_view->bounds().height(), display_height);
}

// Tests that notifications from a single notifier id are grouped in a single
// parent notification view.
TEST_P(NotificationCenterBubbleTest, NotificationsGroupingBasic) {
  const std::string source_url = "http://test-url.com";

  std::string id0, id1;
  id0 = test_api()->AddNotificationWithSourceUrl(source_url);
  id1 = test_api()->AddNotificationWithSourceUrl(source_url);

  // Get the notification id for the parent notification. Parent notifications
  // are created by copying the oldest notification for a given notifier_id.
  const std::string parent_id =
      test_api()->NotificationIdToParentNotificationId(id0);

  test_api()->ToggleBubble();

  auto* parent_notification_view =
      test_api()->GetNotificationViewForId(parent_id);

  // Ensure id0, id1 exist as child notifications inside the
  // `parent_notification_view`.
  EXPECT_TRUE(parent_notification_view->FindGroupNotificationView(id0));
  EXPECT_TRUE(parent_notification_view->FindGroupNotificationView(id1));
}

TEST_P(NotificationCenterBubbleTest,
       NotificationCollapseStatePreservedFromPopup) {
  std::string id0 = test_api()->AddNotification();

  // Manually collapse the notification.
  static_cast<AshNotificationView*>(
      test_api()->GetPopupViewForId(id0)->message_view())
      ->ToggleExpand();

  // Expect `id0` to be collapsed despite the default state for it to be
  // expanded.
  test_api()->ToggleBubble();
  EXPECT_FALSE(test_api()->GetNotificationViewForId(id0)->IsExpanded());
}

TEST_P(NotificationCenterBubbleTest,
       NotificationExpandStatePreservedAcrossDisplays) {
  UpdateDisplay("600x500,600x500");

  std::string id0, id1;
  id0 = test_api()->AddNotification();
  id1 = test_api()->AddNotification();

  const int64_t secondary_display_id = display_manager()->GetDisplayAt(1).id();

  test_api()->ToggleBubbleOnDisplay(secondary_display_id);

  auto* notification_view0 = static_cast<AshNotificationView*>(
      test_api()->GetNotificationViewForIdOnDisplay(id0, secondary_display_id));

  auto* notification_view1 = static_cast<AshNotificationView*>(
      test_api()->GetNotificationViewForIdOnDisplay(id1, secondary_display_id));

  // The newest notification is expected to be expanded by default while other
  // notifications are expected to be collapsed by default.
  EXPECT_TRUE(notification_view1->IsExpanded());
  EXPECT_FALSE(notification_view0->IsExpanded());

  // Toggle the expand states for both notifications so they are opposite of the
  // default state.
  notification_view0->ToggleExpand();
  notification_view1->ToggleExpand();

  const int64_t primary_display_id = display_manager()->GetDisplayAt(0).id();
  test_api()->ToggleBubbleOnDisplay(primary_display_id);

  // Expect the expanded states to be preserved on the primary display after
  // they were changed by the user on the secondary display.
  EXPECT_FALSE(test_api()
                   ->GetNotificationViewForIdOnDisplay(id1, primary_display_id)
                   ->IsExpanded());
  EXPECT_TRUE(test_api()
                  ->GetNotificationViewForIdOnDisplay(id0, primary_display_id)
                  ->IsExpanded());
}

TEST_P(NotificationCenterBubbleTest, LockScreenNotificationVisibility) {
  std::string system_id, id;
  system_id = test_api()->AddSystemNotification();
  id = test_api()->AddNotification();

  GetSessionControllerClient()->LockScreen();

  test_api()->ToggleBubble();

  EXPECT_FALSE(test_api()->GetNotificationViewForId(id));
  EXPECT_TRUE(test_api()->GetNotificationViewForId(system_id));
  EXPECT_TRUE(test_api()->GetNotificationViewForId(system_id)->GetVisible());
}

TEST_P(NotificationCenterBubbleTest, BubbleActivationWithGestureTap) {
  test_api()->AddNotification();

  test_api()->ToggleBubble();
  auto* widget = test_api()->GetWidget();
  EXPECT_FALSE(widget->IsActive());

  GestureTapOn(test_api()->GetNotificationCenterView());
  EXPECT_TRUE(widget->IsActive());
}

TEST_P(NotificationCenterBubbleTest, BubbleActivationWithTouchPress) {
  test_api()->AddNotification();

  test_api()->ToggleBubble();
  auto* widget = test_api()->GetWidget();
  EXPECT_FALSE(widget->IsActive());

  GetEventGenerator()->PressTouch(test_api()
                                      ->GetNotificationCenterView()
                                      ->GetBoundsInScreen()
                                      .CenterPoint());
  EXPECT_TRUE(widget->IsActive());
}

TEST_P(NotificationCenterBubbleTest, BubbleActivationWithMouseClick) {
  test_api()->AddNotification();

  test_api()->ToggleBubble();
  auto* widget = test_api()->GetWidget();
  EXPECT_FALSE(widget->IsActive());

  LeftClickOn(test_api()->GetNotificationCenterView());
  EXPECT_TRUE(widget->IsActive());
}

// Tests that unlocking the device automatically closes the notification bubble.
// See b/287622547.
// TODO(b/347817687): Re-enable test by fixing dangling ptr check.
TEST_P(NotificationCenterBubbleTest, DISABLED_UnlockClosesBubble) {
  // Add a notification so that the notification tray will be visible on the
  // lock screen.
  test_api()->AddNotification();
  ASSERT_GE(1u, test_api()->GetNotificationCount());

  // Show the lock screen.
  GetSessionControllerClient()->LockScreen();
  ASSERT_GE(1u, test_api()->GetNotificationCount());

  // Make the notification bubble visible on the lock screen.
  test_api()->ToggleBubble();
  ASSERT_TRUE(test_api()->IsBubbleShown());

  // Unlock the device without explicitly closing the notification bubble first.
  GetSessionControllerClient()->UnlockScreen();

  // Verify that the notification bubble was automatically closed.
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

TEST_P(NotificationCenterBubbleTest, LargeNotificationExpand) {
  const std::string url = "http://test-url.com/";
  std::string id0 = test_api()->AddNotificationWithSourceUrl(url);

  // Create a large grouped notification by adding a bunch of notifications with
  // the same url.
  for (int i = 0; i < 20; i++) {
    test_api()->AddNotificationWithSourceUrl(url);
  }

  test_api()->ToggleBubble();

  std::string parent_id =
      id0 + message_center_utils::GenerateGroupParentNotificationIdSuffix(
                message_center::MessageCenter::Get()
                    ->FindNotificationById(id0)
                    ->notifier_id());

  auto* parent_notification_view = static_cast<AshNotificationView*>(
      test_api()->GetNotificationViewForId(parent_id));

  EXPECT_FALSE(parent_notification_view->IsExpanded());

  // Expand the grouped notification to overflow the bubble. Then, make sure the
  // `NotificationListView` is sized correctly relative to the notification and
  // the bubble's widget.
  parent_notification_view->ToggleExpand();
  EXPECT_TRUE(parent_notification_view->IsExpanded());
  test_api()->CompleteNotificationListAnimation();

  EXPECT_EQ(parent_notification_view->height(),
            test_api()->GetNotificationListView()->height());

  EXPECT_LT(test_api()->GetWidget()->GetWindowBoundsInScreen().height(),
            test_api()->GetNotificationListView()->height());
}

class NotificationCenterBubbleMultiDisplayTest
    : public NotificationCenterBubbleTestBase,
      public testing::WithParamInterface<
          std::tuple</* Primary display height */ int,
                     /* Secondary display height */ int,
                     /* enable_ongoing_processes */ bool>> {
 public:
  NotificationCenterBubbleMultiDisplayTest()
      : NotificationCenterBubbleTestBase(
            /*enable_ongoing_processes=*/std::get<2>(GetParam())) {}

 protected:
  int GetPrimaryDisplayHeight() { return std::get<0>(GetParam()); }
  int GetSecondaryDisplayHeight() { return std::get<1>(GetParam()); }
};

INSTANTIATE_TEST_SUITE_P(DisplayHeight,
                         NotificationCenterBubbleMultiDisplayTest,
                         testing::Values(
                             // Short primary display, tall secondary display
                             std::make_tuple(600, 1600, false),
                             std::make_tuple(600, 1600, true),
                             // Tall primary display, short secondary display
                             std::make_tuple(1600, 600, false),
                             std::make_tuple(1600, 600, true),
                             // Same primary and secondary display heights
                             std::make_tuple(600, 600, false),
                             std::make_tuple(600, 600, true)));

// Tests that the height of the bubble is constrained according to the
// parameters of the display it is being shown on.
TEST_P(NotificationCenterBubbleMultiDisplayTest,
       BubbleHeightConstrainedByDisplay) {
  UpdateDisplay("800x" + base::NumberToString(GetPrimaryDisplayHeight()) +
                ",800x" + base::NumberToString(GetSecondaryDisplayHeight()));
  const int64_t secondary_display_id = display_manager()->GetDisplayAt(1).id();

  // Add a large number of notifications to overflow the scroll view in the
  // notification center.
  for (int i = 0; i < 100; i++) {
    test_api()->AddNotification();
  }

  // Show the primary display's notification center.
  test_api()->ToggleBubble();

  // The height of the primary display's notification center should not exceed
  // the primary display's height.
  const int bubble1_height =
      test_api()->GetNotificationCenterView()->bounds().height();
  EXPECT_LT(bubble1_height, GetPrimaryDisplayHeight());

  // Show the secondary display's notification center.
  test_api()->ToggleBubbleOnDisplay(secondary_display_id);

  // The height of the secondary display's notification center should not exceed
  // the secondary display's height.
  const int bubble2_height =
      test_api()
          ->GetNotificationCenterViewOnDisplay(secondary_display_id)
          ->bounds()
          .height();
  EXPECT_LT(bubble2_height, GetSecondaryDisplayHeight());

  // The height of the notification center on the taller display should be
  // larger than the height of the notification center on the shorter display,
  // or the heights should be equal if the displays' heights are equal.
  if (GetPrimaryDisplayHeight() > GetSecondaryDisplayHeight()) {
    EXPECT_GT(bubble1_height, bubble2_height);
  } else if (GetPrimaryDisplayHeight() < GetSecondaryDisplayHeight()) {
    EXPECT_LT(bubble1_height, bubble2_height);
  } else {
    EXPECT_EQ(bubble1_height, bubble2_height);
  }
}

}  // namespace ash