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

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/system/notification_center/message_center_utils.h"
#include "ash/system/notification_center/notification_center_test_api.h"
#include "ash/system/privacy/privacy_indicators_controller.h"
#include "ash/system/privacy/privacy_indicators_tray_item_view.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/unified/notification_counter_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/components/login/auth/auth_events_recorder.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/manager/display_manager.h"

namespace ash {

constexpr char kNotificationCenterTrayNoNotificationsToastId[] =
    "notification_center_tray_toast_ids.no_notifications";

class NotificationCenterTrayTestBase : public AshTestBase {
 public:
  NotificationCenterTrayTestBase(bool enable_notification_center_controller)
      : enable_notification_center_controller_(
            enable_notification_center_controller) {
    scoped_feature_list_.InitWithFeatureState(
        features::kNotificationCenterController,
        IsNotificationCenterControllerEnabled());
  }

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

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

  bool IsNotificationCenterControllerEnabled() const {
    return enable_notification_center_controller_;
  }

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

class NotificationCenterTrayTest
    : public NotificationCenterTrayTestBase,
      public testing::WithParamInterface<
          /*enable_notification_center_controller=*/bool> {
 public:
  NotificationCenterTrayTest()
      : NotificationCenterTrayTestBase(
            /*enable_notification_center_controller=*/GetParam()) {}
};

INSTANTIATE_TEST_SUITE_P(
    All,
    NotificationCenterTrayTest,
    /*enable_notification_center_controller=*/testing::Bool());

// Test the initial state.
TEST_P(NotificationCenterTrayTest, ShowTrayButtonOnNotificationAvailability) {
  EXPECT_FALSE(test_api()->GetTray()->GetVisible());

  std::string id = test_api()->AddNotification();
  EXPECT_TRUE(test_api()->GetTray()->GetVisible());

  message_center::MessageCenter::Get()->RemoveNotification(id, true);

  EXPECT_FALSE(test_api()->GetTray()->GetVisible());
}

// Bubble creation and destruction.
TEST_P(NotificationCenterTrayTest, ShowAndHideBubbleOnUserInteraction) {
  test_api()->AddNotification();

  auto* tray = test_api()->GetTray();

  // Clicking on the tray button should show  the bubble.
  LeftClickOn(tray);
  EXPECT_TRUE(test_api()->IsBubbleShown());

  // Clicking a second time should destroy the bubble.
  LeftClickOn(test_api()->GetTray());
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

// Hitting escape after opening the bubble should destroy the bubble
// gracefully.
TEST_P(NotificationCenterTrayTest, EscapeClosesBubble) {
  auto* tray = test_api()->GetTray();
  LeftClickOn(tray);
  PressAndReleaseKey(ui::KeyboardCode::VKEY_ESCAPE);
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

// Removing all notifications by hitting the `clear_all_button_` should result
// in the bubble being destroyed and the tray bubble going invisible.
TEST_P(NotificationCenterTrayTest,
       ClearAllNotificationsDestroysBubbleAndHidesTray) {
  test_api()->AddNotification();
  test_api()->AddNotification();
  test_api()->AddNotification();

  auto* tray = test_api()->GetTray();

  LeftClickOn(tray);
  LeftClickOn(test_api()->GetClearAllButton());
  EXPECT_FALSE(test_api()->IsBubbleShown());
  EXPECT_FALSE(test_api()->IsTrayShown());
}

// The last notification being removed directly by the
// `message_center::MessageCenter` API should result in the bubble being
// destroyed and tray visibility being updated.
TEST_P(NotificationCenterTrayTest, NotificationsRemovedByMessageCenterApi) {
  std::string id = test_api()->AddNotification();
  test_api()->RemoveNotification(id);

  EXPECT_FALSE(test_api()->IsBubbleShown());
  EXPECT_FALSE(test_api()->IsTrayShown());
}

// When the only notifications present are all in the same group, removing the
// parent notification of that group should result in the bubble being destroyed
// and the tray being hidden.
TEST_P(NotificationCenterTrayTest,
       RemovingGroupParentDestroysBubbleAndHidesTray) {
  // Add two notifications that belong to the same group.
  const std::string url = "http://test-url.com";
  const std::string id0 = test_api()->AddNotificationWithSourceUrl(url);
  const std::string id1 = test_api()->AddNotificationWithSourceUrl(url);
  ASSERT_EQ(message_center_utils::GetNotificationCount(), 1u);
  ASSERT_TRUE(test_api()->IsTrayShown());

  // Show the bubble.
  test_api()->ToggleBubble();
  ASSERT_TRUE(test_api()->IsBubbleShown());

  // Remove the parent notification.
  const std::string parent_id =
      test_api()->NotificationIdToParentNotificationId(id0);
  test_api()->RemoveNotification(parent_id);

  // Verify that the bubble and tray are both gone.
  EXPECT_FALSE(test_api()->IsBubbleShown());
  EXPECT_FALSE(test_api()->IsTrayShown());
}

// Tests that clicking on the tray immediately after all notifications have been
// removed does not result in an empty bubble being shown. This addresses
// b/293174118.
TEST_P(NotificationCenterTrayTest, ClickOnTrayAfterRemovingNotifications) {
  // Add a notification to make the tray visible. Note that animations complete
  // immediately in this part of the test.
  std::string id = test_api()->AddNotification();
  auto* tray = test_api()->GetTray();

  // Make sure animations don't complete immediately for the rest of the test,
  // so that we can click on the tray while its running its hide animation.
  ui::ScopedAnimationDurationScaleMode animation_duration_scale(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Remove all notifications and verify that the tray's hide animation is
  // running.
  test_api()->RemoveNotification(id);
  ASSERT_EQ(0u, test_api()->GetNotificationCount());
  EXPECT_TRUE(test_api()->IsTrayAnimating());
  EXPECT_FALSE(test_api()->GetTray()->layer()->GetTargetVisibility());
  EXPECT_EQ(test_api()->GetTray()->layer()->GetTargetOpacity(), 0);

  // Click on the tray and verify that notification bubble does not show (we do
  // not need to wait for animations here because if the bubble were to be shown
  // it would happen immediately, without animation, after clicking the tray).
  LeftClickOn(tray);
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

// Tests that opening the bubble results in existing popups being dismissed
// and no new ones being created.
TEST_P(NotificationCenterTrayTest, NotificationPopupsHiddenWithBubble) {
  // Adding a notification should generate a popup.
  std::string id = test_api()->AddNotification();
  EXPECT_TRUE(test_api()->IsPopupShown(id));

  // Opening the notification center should result in the popup being dismissed.
  test_api()->ToggleBubble();
  EXPECT_FALSE(test_api()->IsPopupShown(id));

  // New notifications should not generate popups while the notification center
  // is visible.
  std::string id2 = test_api()->AddNotification();
  EXPECT_FALSE(test_api()->IsPopupShown(id));
}

// Tests that popups are shown after the notification center is closed.
TEST_P(NotificationCenterTrayTest, NotificationPopupsShownAfterBubbleClose) {
  test_api()->AddNotification();

  // Open and close bubble to dismiss existing popups.
  test_api()->ToggleBubble();
  test_api()->ToggleBubble();

  // New notifications should show up as popups after the bubble is closed.
  std::string id = test_api()->AddNotification();
  EXPECT_TRUE(test_api()->IsPopupShown(id));
}

// Keyboard accelerator shows/hides the bubble.
TEST_P(NotificationCenterTrayTest, AcceleratorTogglesBubble) {
  test_api()->AddNotification();
  EXPECT_FALSE(test_api()->IsBubbleShown());
  // Pressing the accelerator should show the bubble.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_N, ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN));
  EXPECT_TRUE(test_api()->IsBubbleShown());
  // Pressing the acccelerator again should hide the bubble.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_N, ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN));
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

// Keyboard accelerator shows a toast when there are no notifications.
TEST_P(NotificationCenterTrayTest, AcceleratorShowsToastWhenNoNotifications) {
  ASSERT_EQ(test_api()->GetNotificationCount(), 0u);
  EXPECT_FALSE(ToastManager::Get()->IsToastShown(
      kNotificationCenterTrayNoNotificationsToastId));
  // Pressing the accelerator should show the toast and not the bubble.
  ShellTestApi().PressAccelerator(
      ui::Accelerator(ui::VKEY_N, ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN));
  EXPECT_TRUE(ToastManager::Get()->IsToastShown(
      kNotificationCenterTrayNoNotificationsToastId));
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

// Tests that the bubble automatically hides if it is visible when another
// bubble becomes visible, and otherwise does not automatically show or hide.
TEST_P(NotificationCenterTrayTest, BubbleHideBehavior) {
  // Basic verification test that the notification center tray bubble can
  // show/hide itself when no other bubbles are visible.
  EXPECT_FALSE(test_api()->IsBubbleShown());
  test_api()->AddNotification();
  test_api()->ToggleBubble();
  EXPECT_TRUE(test_api()->IsBubbleShown());
  test_api()->ToggleBubble();
  EXPECT_FALSE(test_api()->IsBubbleShown());

  // Test that the notification center tray bubble automatically hides when it
  // is currently visible while another bubble becomes visible.
  test_api()->ToggleBubble();
  EXPECT_TRUE(test_api()->IsBubbleShown());
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_FALSE(test_api()->IsBubbleShown());

  // Hide all currently visible bubbles.
  GetPrimaryUnifiedSystemTray()->CloseBubble();
  EXPECT_FALSE(test_api()->IsBubbleShown());

  // Test that the notification center tray bubble stays hidden when showing
  // another bubble.
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_FALSE(test_api()->IsBubbleShown());
}

// Tests that visibility of the Do not disturb icon changes with Do not disturb
// mode.
TEST_P(NotificationCenterTrayTest, DoNotDisturbIconVisibility) {
  // Test the case where the tray is not initially visible.
  ASSERT_FALSE(test_api()->IsTrayShown());
  EXPECT_FALSE(test_api()->IsDoNotDisturbIconShown());
  message_center::MessageCenter::Get()->SetQuietMode(true);
  EXPECT_TRUE(test_api()->IsTrayShown());
  EXPECT_TRUE(test_api()->IsDoNotDisturbIconShown());
  message_center::MessageCenter::Get()->SetQuietMode(false);
  EXPECT_FALSE(test_api()->IsTrayShown());
  EXPECT_FALSE(test_api()->IsDoNotDisturbIconShown());

  // Test the case where the tray is initially visible.
  test_api()->AddNotification();
  ASSERT_TRUE(test_api()->IsTrayShown());
  EXPECT_FALSE(test_api()->IsDoNotDisturbIconShown());
  message_center::MessageCenter::Get()->SetQuietMode(true);
  EXPECT_TRUE(test_api()->IsTrayShown());
  EXPECT_TRUE(test_api()->IsDoNotDisturbIconShown());
  message_center::MessageCenter::Get()->SetQuietMode(false);
  EXPECT_TRUE(test_api()->IsTrayShown());
  EXPECT_FALSE(test_api()->IsDoNotDisturbIconShown());
}

TEST_P(NotificationCenterTrayTest, DoNotDisturbUpdatesPinnedIcons) {
  test_api()->AddPinnedNotification();
  EXPECT_TRUE(test_api()->IsNotificationIconShown());

  message_center::MessageCenter::Get()->SetQuietMode(true);
  EXPECT_FALSE(test_api()->IsNotificationIconShown());

  message_center::MessageCenter::Get()->SetQuietMode(false);
  EXPECT_TRUE(test_api()->IsNotificationIconShown());
}

TEST_P(NotificationCenterTrayTest, NoPrivacyIndicatorsWhenVcEnabled) {
  // No privacy indicators when `kVideoConference` is enabled.
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(
      features::kFeatureManagementVideoConference);

  auto notification_tray =
      std::make_unique<NotificationCenterTray>(GetPrimaryShelf());
  EXPECT_FALSE(notification_tray->privacy_indicators_view());
}

// Tests that the focus ring is visible and has proper size when the
// notification center tray is focused.
TEST_P(NotificationCenterTrayTest, FocusRing) {
  // Add a notification to make the notification center tray visible.
  test_api()->AddNotification();
  ASSERT_TRUE(test_api()->IsTrayShown());

  // Verify that the focus ring is not already visible.
  EXPECT_FALSE(test_api()->GetFocusRing()->GetVisible());

  // Focus the notification center tray.
  test_api()->FocusTray();

  // Verify that the focus ring is visible and is larger than the notification
  // center tray by `kTrayBackgroundFocusPadding`.
  EXPECT_TRUE(test_api()->GetFocusRing()->GetVisible());
  EXPECT_EQ(test_api()->GetFocusRing()->size(),
            test_api()->GetTray()->size() + kTrayBackgroundFocusPadding.size());
}

// Tests that removing all notifications while the lock screen is showing hides
// the tray.
TEST_P(NotificationCenterTrayTest,
       RemovingAllNotificationsOnLockScreenHidesTray) {
  // Add a notification to make the notification center tray visible.
  const std::string id = test_api()->AddNotification();

  // Show the lock screen.
  BlockUserSession(BLOCKED_BY_LOCK_SCREEN);
  ASSERT_TRUE(test_api()->IsTrayShown());

  // Remove the notification.
  test_api()->RemoveNotification(id);

  // Verify that the tray is hidden.
  EXPECT_FALSE(test_api()->IsTrayShown());
}

// Tests that adding an initial notification while the lock screen is showing
// shows the tray.
TEST_P(NotificationCenterTrayTest, AddingNotificationOnLockScreenShowsTray) {
  // Show the lock screen.
  BlockUserSession(BLOCKED_BY_LOCK_SCREEN);
  ASSERT_FALSE(test_api()->IsTrayShown());

  // Add a notification.
  test_api()->AddNotification();

  // Verify that the tray is shown.
  EXPECT_TRUE(test_api()->IsTrayShown());
}

// Tests that `NotificationCounterView` is not still visible on secondary
// display after logging in with a pinned notification present. This covers
// b/284139989.
TEST_P(NotificationCenterTrayTest,
       NotificationCounterVisibilityForMultiDisplay) {
  // The behavior under test relies on `TrayItemView` animations being
  // scheduled, but `TrayItemView` animations are bypassed when the animation
  // duration scale mode is set to ZERO_DURATION. Hence, set the animation
  // duration scale mode to something else for this test.
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // This test relies on the lock screen actually being created (and creating
  // the lock screen requires the existence of an `AuthEventsRecorder`).
  std::unique_ptr<AuthEventsRecorder> auth_events_recorder =
      AuthEventsRecorder::CreateForTesting();
  GetSessionControllerClient()->set_show_lock_screen_views(true);

  // Create two displays.
  UpdateDisplay("800x799,800x799");
  auto secondary_display_id = display_manager()->GetDisplayAt(1).id();
  auto* secondary_notification_center_tray =
      test_api()->GetTrayOnDisplay(secondary_display_id);
  auto* secondary_notification_counter_view =
      secondary_notification_center_tray->notification_icons_controller()
          ->notification_counter_view();

  // Add a pinned notification.
  test_api()->AddPinnedNotification();

  // Verify that the secondary display's notification center tray shows an icon
  // for the pinned notification and not the `NotificationCounterView`.
  ASSERT_TRUE(
      test_api()->IsNotificationIconShownOnDisplay(secondary_display_id));
  ASSERT_FALSE(secondary_notification_counter_view->GetVisible());

  // Go to the lock screen.
  GetSessionControllerClient()->LockScreen();

  // Log back in.
  GetSessionControllerClient()->UnlockScreen();

  // Verify that the `NotificationCounterView` on the secondary display is not
  // visible.
  EXPECT_FALSE(secondary_notification_counter_view->GetVisible());
}

// Tests that the privacy indicators view is created and show/hide accordingly
// when updated.
TEST_P(NotificationCenterTrayTest, PrivacyIndicatorsVisibility) {
  auto* notification_tray = StatusAreaWidgetTestHelper::GetStatusAreaWidget()
                                ->notification_center_tray();
  auto* privacy_indicators_view = notification_tray->privacy_indicators_view();
  EXPECT_TRUE(privacy_indicators_view);

  EXPECT_FALSE(privacy_indicators_view->GetVisible());

  scoped_refptr<PrivacyIndicatorsNotificationDelegate> delegate =
      base::MakeRefCounted<PrivacyIndicatorsNotificationDelegate>();

  // Updates the controller to simulate camera access, the privacy indicators
  // should become visible.
  PrivacyIndicatorsController::Get()->UpdatePrivacyIndicators(
      /*app_id=*/"app_id", /*app_name=*/u"App Name",
      /*is_camera_used=*/true,
      /*is_microphone_used=*/false, delegate, PrivacyIndicatorsSource::kApps);
  EXPECT_TRUE(privacy_indicators_view->GetVisible());

  // Updates the controller to simulate that camera and microphone are not
  // accessed, the privacy indicators should be hidden.
  PrivacyIndicatorsController::Get()->UpdatePrivacyIndicators(
      /*app_id=*/"app_id", /*app_name=*/u"App Name",
      /*is_camera_used=*/false,
      /*is_microphone_used=*/false, delegate, PrivacyIndicatorsSource::kApps);
  EXPECT_FALSE(privacy_indicators_view->GetVisible());
}

// Test fixture that disables notification popups.
class NotificationCenterTrayNoPopupsTest
    : public NotificationCenterTrayTestBase,
      public testing::WithParamInterface<
          /*enable_notification_center_controller=*/bool> {
 public:
  NotificationCenterTrayNoPopupsTest()
      : NotificationCenterTrayTestBase(
            /*enable_notification_center_controller=*/GetParam()) {}

  void SetUp() override {
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kSuppressMessageCenterPopups);
    NotificationCenterTrayTestBase::SetUp();
  }
};

INSTANTIATE_TEST_SUITE_P(
    All,
    NotificationCenterTrayNoPopupsTest,
    /*enable_notification_center_controller=*/testing::Bool());

// Tests that `NotificationCenterTray`'s `TrayItemView`s show up when adding a
// secondary display. Notification popups are disabled for this test because the
// presence of a popup actually hides the issue (i.e. the secondary display's
// `NotificationCenterTray`'s `TrayItemView`s work as intended when a popup is
// present). This covers b/281158734.
TEST_P(NotificationCenterTrayNoPopupsTest,
       TrayItemsVisibleWhenAddingSecondaryDisplay) {
  // Start with one display.
  UpdateDisplay("800x799");

  // Add a pinned notification and a non-pinned notification.
  test_api()->AddNotification();
  test_api()->AddPinnedNotification();

  // Verify that both the notification counter as well as an icon for the pinned
  // notification are visible in the notification center tray.
  ASSERT_TRUE(test_api()->IsNotificationIconShown());
  ASSERT_TRUE(test_api()->IsNotificationCounterShown());

  // Add a secondary display.
  UpdateDisplay("800x799,800x799");
  auto secondary_display_id = display_manager()->GetDisplayAt(1).id();

  // Verify that both the notification counter as well as an icon for the pinned
  // notification are visible in the secondary display's notification center
  // tray.
  EXPECT_TRUE(
      test_api()->IsNotificationIconShownOnDisplay(secondary_display_id));
  EXPECT_TRUE(
      test_api()->IsNotificationCounterShownOnDisplay(secondary_display_id));
}

// TODO(b/252875025):
// Add following test cases as we add relevant functionality:
// - Focus Change dismissing bubble
// - Popup notifications are dismissed when the bubble appears.
// - Display removed while the bubble is shown.
// - Tablet mode transition with the bubble open.

}  // namespace ash