chromium/ash/system/tray/tray_event_filter_unittest.cc

// Copyright 2017 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/tray/tray_event_filter.h"

#include "ash/constants/ash_features.h"
#include "ash/ime/ime_controller_impl.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/system/ime_menu/ime_menu_tray.h"
#include "ash/system/notification_center/ash_message_popup_collection.h"
#include "ash/system/notification_center/views/ash_notification_expand_button.h"
#include "ash/system/notification_center/views/ash_notification_view.h"
#include "ash/system/network/network_detailed_view.h"
#include "ash/system/notification_center/notification_center_tray.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/tray/tray_utils.h"
#include "ash/system/unified/date_tray.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
#include "ui/gfx/geometry/size.h"
#include "ui/message_center/message_center.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/window_util.h"

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

namespace ash {

namespace {

class TestTrayBackgroundView : public TrayBackgroundView {
 public:
  explicit TestTrayBackgroundView(Shelf* shelf)
      : TrayBackgroundView(shelf,
                           TrayBackgroundViewCatalogName::kTestCatalogName,
                           RoundedCornerBehavior::kAllRounded) {
    SetCallback(base::BindRepeating(&TestTrayBackgroundView::OnButtonPressed,
                                    base::Unretained(this)));
  }

  TestTrayBackgroundView(const TestTrayBackgroundView&) = delete;
  TestTrayBackgroundView& operator=(const TestTrayBackgroundView&) = delete;

  ~TestTrayBackgroundView() override = default;

  // TrayBackgroundView:
  void ClickedOutsideBubble(const ui::LocatedEvent& event) override {
    clicked_outside_bubble_called_ = true;
    CloseBubble();
  }

  void UpdateTrayItemColor(bool is_active) override {}
  std::u16string GetAccessibleNameForTray() override {
    return u"TestTrayBackgroundView";
  }

  void HandleLocaleChange() override {}

  void HideBubbleWithView(const TrayBubbleView* bubble_view) override {
    if (bubble_view == bubble_->GetBubbleView()) {
      CloseBubble();
    }
  }

  void HideBubble(const TrayBubbleView* bubble_view) override {
    if (bubble_view == bubble_->GetBubbleView()) {
      CloseBubble();
    }
  }

  void ShowBubble() override {
    auto bubble_view = std::make_unique<TrayBubbleView>(
        CreateInitParamsForTrayBubble(/*tray=*/this));
    bubble_view->SetPreferredSize(gfx::Size(kTrayMenuWidth, 100));
    bubble_ = std::make_unique<TrayBubbleWrapper>(this,
                                                  /*event_handling=*/true);
    bubble_->ShowBubble(std::move(bubble_view));
  }

  void CloseBubbleInternal() override { bubble_.reset(); }

  TrayBubbleWrapper* bubble() { return bubble_.get(); }

  bool clicked_outside_bubble_called() const {
    return clicked_outside_bubble_called_;
  }

 private:
  void OnButtonPressed(const ui::Event& event) {
    if (bubble_) {
      CloseBubble();
      return;
    }
    ShowBubble();
  }

  std::unique_ptr<TrayBubbleWrapper> bubble_;
  bool clicked_outside_bubble_called_ = false;
};

}  // namespace

class TrayEventFilterTest : public AshTestBase {
 public:
  TrayEventFilterTest() = default;

  TrayEventFilterTest(const TrayEventFilterTest&) = delete;
  TrayEventFilterTest& operator=(const TrayEventFilterTest&) = delete;

  ~TrayEventFilterTest() override = default;

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

    // Adds this `test_tray_background_view_` to the mock `StatusAreaWidget`.
    // Can't use std::make_unique() here, because we need base class type for
    // template method to link successfully without adding test code to
    // status_area_widget.cc.
    test_tray_background_view_ = static_cast<TestTrayBackgroundView*>(
        StatusAreaWidgetTestHelper::GetStatusAreaWidget()->AddTrayButton(
            std::unique_ptr<TrayBackgroundView>(
                new TestTrayBackgroundView(GetPrimaryShelf()))));

    test_tray_background_view_->SetVisiblePreferred(true);
  }

  void TearDown() override {
    test_tray_background_view_ = nullptr;
    AshTestBase::TearDown();
  }

  void ClickInsideWidget(views::Widget* widget) {
    auto* event_generator = GetEventGenerator();
    event_generator->MoveMouseTo(
        widget->GetWindowBoundsInScreen().CenterPoint());
    event_generator->ClickLeftButton();
  }

  void ClickOutsideWidget(views::Widget* widget) {
    auto* event_generator = GetEventGenerator();
    event_generator->MoveMouseTo(widget->GetWindowBoundsInScreen().origin() -
                                 gfx::Vector2d(1, 1));
    event_generator->ClickLeftButton();
  }

 protected:
  std::string AddNotification() {
    std::string notification_id = base::NumberToString(notification_id_++);
    MessageCenter::Get()->AddNotification(std::make_unique<Notification>(
        message_center::NOTIFICATION_TYPE_SIMPLE, notification_id,
        u"test title", u"test message", ui::ImageModel(),
        std::u16string() /* display_source */, GURL(),
        message_center::NotifierId(), message_center::RichNotificationData(),
        new message_center::NotificationDelegate()));
    return notification_id;
  }

  void ShowTestBubble() { test_tray_background_view_->ShowBubble(); }

  views::Widget* GetTestBubbleWidget() {
    if (!test_tray_background_view_->bubble()) {
      return nullptr;
    }

    return test_tray_background_view_->bubble()->GetBubbleWidget();
  }

  void ShowQuickSettingsBubble() {
    GetPrimaryUnifiedSystemTray()->ShowBubble();
  }

  bool IsQuickSettingsBubbleShown() {
    return GetPrimaryUnifiedSystemTray()->IsBubbleShown();
  }

  UnifiedSystemTray* GetPrimaryUnifiedSystemTray() {
    return GetPrimaryShelf()->GetStatusAreaWidget()->unified_system_tray();
  }

  void AnimatePopupAnimationUntilIdle() {
    AshMessagePopupCollection* popup_collection =
        GetPrimaryNotificationCenterTray()->popup_collection();

    while (popup_collection->animation()->is_animating()) {
      popup_collection->animation()->SetCurrentValue(1.0);
      popup_collection->animation()->End();
    }
  }

  TestTrayBackgroundView* test_tray_background_view() {
    return test_tray_background_view_;
  }

 private:
  int notification_id_ = 0;
  raw_ptr<TestTrayBackgroundView> test_tray_background_view_ = nullptr;
};

// Tests that clicking on notification popup when bubble is open will not result
// in the bubble closes. The logic for this is handled in
// `bubble_utils::ShouldCloseBubbleForEvent()` where we ignore events happen
// inside a `kShellWindowId_SettingBubbleContainer`.
TEST_F(TrayEventFilterTest, ClickOnPopupWhenBubbleOpen) {
  // Update display so that the screen is height enough and expand/collapse
  // notification is allowed on top of the tray bubble.
  UpdateDisplay("901x900");

  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(features::kNotifierCollision);

  ShowQuickSettingsBubble();
  EXPECT_TRUE(IsQuickSettingsBubbleShown());

  auto notification_id = AddNotification();
  auto* popup_view = GetPrimaryNotificationCenterTray()
                         ->popup_collection()
                         ->GetMessageViewForNotificationId(notification_id);

  auto* ash_notification_popup = static_cast<AshNotificationView*>(popup_view);

  AnimatePopupAnimationUntilIdle();

  // Collapsing the popup should not close the bubble.
  LeftClickOn(ash_notification_popup->expand_button_for_test());
  // Wait until the animation is complete.
  AnimatePopupAnimationUntilIdle();
  EXPECT_FALSE(ash_notification_popup->IsExpanded());
  EXPECT_TRUE(IsQuickSettingsBubbleShown());

  // Expanding the popup should not close the bubble.
  LeftClickOn(ash_notification_popup->expand_button_for_test());
  // Wait until the animation is complete.
  AnimatePopupAnimationUntilIdle();
  EXPECT_TRUE(ash_notification_popup->IsExpanded());
  EXPECT_TRUE(IsQuickSettingsBubbleShown());
}

TEST_F(TrayEventFilterTest, DraggingInsideDoesNotCloseBubble) {
  ShowTestBubble();
  auto* bubble_widget = GetTestBubbleWidget();
  EXPECT_TRUE(bubble_widget);

  // Dragging within the bubble should not trigger `ClickedOutsideBubble()` and
  // close the bubble.
  const gfx::Rect tray_bounds = bubble_widget->GetWindowBoundsInScreen();
  auto* test_tray = test_tray_background_view();
  auto border_insets = test_tray->bubble()->bubble_view()->GetBorderInsets();
  const gfx::Point start =
      tray_bounds.origin() +
      gfx::Vector2d(border_insets.left(), border_insets.top());
  const gfx::Point end_inside = start + gfx::Vector2d(5, 5);
  GetEventGenerator()->GestureScrollSequence(start, end_inside,
                                             base::Milliseconds(100), 4);

  EXPECT_FALSE(test_tray_background_view()->clicked_outside_bubble_called());
  EXPECT_TRUE(test_tray_background_view()->bubble());

  // Dragging from inside to outside of the bubble should not trigger
  // `ClickedOutsideBubble()` and close the bubble.
  const gfx::Point start_inside = end_inside;
  const gfx::Point end_outside = start + gfx::Vector2d(-5, -5);
  GetEventGenerator()->GestureScrollSequence(start_inside, end_outside,
                                             base::Milliseconds(100), 4);

  EXPECT_FALSE(test_tray_background_view()->clicked_outside_bubble_called());
  EXPECT_TRUE(test_tray_background_view()->bubble());
}

TEST_F(TrayEventFilterTest, DraggingOnTrayClosesBubble) {
  ShowTestBubble();
  EXPECT_TRUE(GetTestBubbleWidget());

  // Dragging on the tray background view should trigger
  // `ClickedOutsideBubble()` and close the bubble.
  const gfx::Rect tray_bounds =
      test_tray_background_view()->GetBoundsInScreen();
  const gfx::Point start = tray_bounds.CenterPoint();
  const gfx::Point end_inside = start + gfx::Vector2d(0, 10);
  GetEventGenerator()->GestureScrollSequence(start, end_inside,
                                             base::Milliseconds(100), 4);

  EXPECT_TRUE(test_tray_background_view()->clicked_outside_bubble_called());
  EXPECT_FALSE(test_tray_background_view()->bubble());
}

// Tests that when we drag up to show the hotseat, the open bubble will be close
// to make sure it does not overlap with the hotseat (crbug/1329327).
TEST_F(TrayEventFilterTest, ShowHotseatClosesBubble) {
  TabletModeControllerTestApi().EnterTabletMode();
  std::unique_ptr<aura::Window> window =
      CreateTestWindow(gfx::Rect(0, 0, 400, 400));
  wm::ActivateWindow(window.get());
  ASSERT_EQ(HotseatState::kHidden,
            GetPrimaryShelf()->shelf_layout_manager()->hotseat_state());

  ShowTestBubble();
  EXPECT_TRUE(GetTestBubbleWidget());

  // Dragging up to show the hotseat.
  gfx::Rect display_bounds =
      display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
  const gfx::Point start = display_bounds.bottom_center();
  const gfx::Point end = start + gfx::Vector2d(0, -80);
  GetEventGenerator()->GestureScrollSequence(
      start, end, /*duration=*/base::Milliseconds(100),
      /*steps=*/4);
  ASSERT_EQ(HotseatState::kExtended,
            GetPrimaryShelf()->shelf_layout_manager()->hotseat_state());

  // `ClickedOutsideBubble()` should be triggered to close the bubble.
  EXPECT_TRUE(test_tray_background_view()->clicked_outside_bubble_called());
  EXPECT_FALSE(test_tray_background_view()->bubble());
}

TEST_F(TrayEventFilterTest, ClickOnCalendarBubbleClosesOtherTrays) {
  Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
  auto* status_area = GetPrimaryShelf()->GetStatusAreaWidget();
  auto* ime_tray = status_area->ime_menu_tray();

  LeftClickOn(ime_tray);
  EXPECT_TRUE(ime_tray->GetBubbleWidget());

  auto* date_tray = status_area->date_tray();
  LeftClickOn(date_tray);

  // When opening the calendar, the unified system tray bubble should be open
  // with the calendar view, and the IME bubble should be closed.
  EXPECT_TRUE(IsQuickSettingsBubbleShown());
  EXPECT_FALSE(ime_tray->GetBubbleWidget());
}

// Tests that when we open the calendar while Quick Settings bubble is open, the
// bubble will not be closed.
TEST_F(TrayEventFilterTest, TransitionFromQsToCalendar) {
  ShowQuickSettingsBubble();
  EXPECT_TRUE(IsQuickSettingsBubbleShown());

  LeftClickOn(GetPrimaryShelf()->GetStatusAreaWidget()->date_tray());
  EXPECT_TRUE(IsQuickSettingsBubbleShown());
}

TEST_F(TrayEventFilterTest, CloseTrayBubbleWhenWindowActivated) {
  StatusAreaWidget* status_area = GetPrimaryShelf()->GetStatusAreaWidget();
  UnifiedSystemTray* system_tray = status_area->unified_system_tray();

  LeftClickOn(system_tray);
  ASSERT_EQ(status_area->open_shelf_pod_bubble(),
            system_tray->bubble()->GetBubbleView());

  // Showing a new window and activating it will close the system bubble.
  std::unique_ptr<views::Widget> widget(
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET));
  EXPECT_TRUE(widget->IsActive());
  EXPECT_FALSE(system_tray->bubble());

  // Show a second widget.
  std::unique_ptr<views::Widget> second_widget(
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET));
  EXPECT_TRUE(second_widget->IsActive());

  // Re-show the system bubble.
  LeftClickOn(system_tray);

  // Re-activate the first widget. The system bubble should hide again.
  widget->Activate();
  EXPECT_FALSE(system_tray->bubble());

  Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
  TrayBackgroundView* ime_menu = status_area->ime_menu_tray();

  // Test the same thing with the ime tray.
  LeftClickOn(ime_menu);
  ASSERT_EQ(status_area->open_shelf_pod_bubble(), ime_menu->GetBubbleView());

  second_widget->Activate();
  EXPECT_FALSE(ime_menu->GetBubbleView());
}

TEST_F(TrayEventFilterTest, NotCloseTrayBubbleWhenTranscientChildActivated) {
  UnifiedSystemTray* system_tray =
      GetPrimaryShelf()->GetStatusAreaWidget()->unified_system_tray();

  ShowQuickSettingsBubble();

  auto* bubble = system_tray->bubble();

  // Show the network detailed view.
  bubble->unified_system_tray_controller()->ShowNetworkDetailedView();

  // Click on the info button in the network detailed view so that a transient
  // bubble is opened.
  auto* info_button = bubble->quick_settings_view()
                          ->GetDetailedViewForTest<NetworkDetailedView>()
                          ->info_button_for_testing();
  LeftClickOn(info_button);

  // Since a transcient child of the bubble is activated, the bubble should
  // remain open.
  EXPECT_TRUE(system_tray->bubble());
}

}  // namespace ash