chromium/ash/public/cpp/external_arc/message_center/arc_notification_view_unittest.cc

// Copyright 2016 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/public/cpp/external_arc/message_center/arc_notification_view.h"

#include <memory>

#include "ash/public/cpp/external_arc/message_center/arc_notification_content_view.h"
#include "ash/public/cpp/external_arc/message_center/arc_notification_item.h"
#include "ash/public/cpp/external_arc/message_center/arc_notification_surface.h"
#include "ash/public/cpp/external_arc/message_center/mock_arc_notification_item.h"
#include "ash/public/cpp/external_arc/message_center/mock_arc_notification_surface.h"
#include "ash/public/cpp/message_center/arc_notification_constants.h"
#include "ash/shell.h"
#include "ash/system/notification_center/message_view_factory.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/desks/desks_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/ime/dummy_text_input_client.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/notification_control_buttons_view.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/test/views_test_base.h"

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

namespace ash {

namespace {

class TestTextInputClient : public ui::DummyTextInputClient {
 public:
  TestTextInputClient() : ui::DummyTextInputClient(ui::TEXT_INPUT_TYPE_TEXT) {}

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

  ui::TextInputType GetTextInputType() const override { return type_; }

  void set_text_input_type(ui::TextInputType type) { type_ = type; }

 private:
  ui::TextInputType type_ = ui::TEXT_INPUT_TYPE_NONE;
};

}  // namespace

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

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

  ~ArcNotificationViewTest() override = default;

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

    item_ = std::make_unique<MockArcNotificationItem>(kDefaultNotificationKey);

    MessageViewFactory::ClearCustomNotificationViewFactory(
        kArcNotificationCustomViewType);
    MessageViewFactory::SetCustomNotificationViewFactory(
        kArcNotificationCustomViewType,
        base::BindRepeating(
            &ArcNotificationViewTest::CreateCustomMessageViewForTest,
            base::Unretained(this), item_.get()));

    std::unique_ptr<Notification> notification = CreateSimpleNotification();

    std::unique_ptr<ArcNotificationView> notification_view(
        static_cast<ArcNotificationView*>(
            MessageViewFactory::Create(*notification, /*shown_in_popup=*/false)
                .release()));
    notification_view_ = notification_view.get();
    surface_ =
        std::make_unique<MockArcNotificationSurface>(kDefaultNotificationKey);
    notification_view_->content_view_->SetSurface(surface_.get());
    UpdateNotificationViews(*notification);

    views::Widget::InitParams init_params(
        views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
        views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
    init_params.context = GetContext();
    init_params.parent = Shell::GetPrimaryRootWindow()->GetChildById(
        desks_util::GetActiveDeskContainerId());
    views::Widget* widget = new views::Widget();
    widget->Init(std::move(init_params));
    widget->SetContentsView(std::move(notification_view));
    widget->SetSize(notification_view_->GetPreferredSize());
    widget->Show();
    EXPECT_EQ(widget, notification_view_->GetWidget());
  }

  std::unique_ptr<Notification> CreateSimpleNotification() {
    std::unique_ptr<Notification> notification = std::make_unique<Notification>(
        message_center::NOTIFICATION_TYPE_CUSTOM, kDefaultNotificationId,
        u"title", u"message", ui::ImageModel(), u"display source", GURL(),
        message_center::NotifierId(
            message_center::NotifierType::ARC_APPLICATION, "test_app_id"),
        message_center::RichNotificationData(), nullptr);

    notification->set_custom_view_type(kArcNotificationCustomViewType);
    return notification;
  }

  void TearDown() override {
    widget()->Close();
    item_.reset();
    notification_.reset();
    surface_.reset();
    AshTestBase::TearDown();
  }

  void PerformClick(const gfx::Point& point) {
    ui::MouseEvent pressed_event = ui::MouseEvent(
        ui::EventType::kMousePressed, point, point, ui::EventTimeForNow(),
        ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON);
    widget()->OnMouseEvent(&pressed_event);
    ui::MouseEvent released_event = ui::MouseEvent(
        ui::EventType::kMouseReleased, point, point, ui::EventTimeForNow(),
        ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON);
    widget()->OnMouseEvent(&released_event);
  }

  void PerformKeyEvents(ui::KeyboardCode code) {
    ui::KeyEvent event1 =
        ui::KeyEvent(ui::EventType::kKeyPressed, code, ui::EF_NONE);
    widget()->OnKeyEvent(&event1);
    ui::KeyEvent event2 =
        ui::KeyEvent(ui::EventType::kKeyReleased, code, ui::EF_NONE);
    widget()->OnKeyEvent(&event2);
  }

  void UpdateNotificationViews(const Notification& notification) {
    MessageCenter::Get()->AddNotification(
        std::make_unique<Notification>(notification));
    notification_view_->UpdateWithNotification(notification);
  }

  float GetNotificationSlideAmount() const {
    return notification_view_->GetSlideOutLayer()
        ->transform()
        .To2dTranslation()
        .x();
  }

  bool IsPopupRemovedAfterIdle(const std::string& notification_id) const {
    base::RunLoop().RunUntilIdle();
    return !MessageCenter::Get()->FindPopupNotificationById(notification_id);
  }

  bool IsRemovedAfterIdle(const std::string& notification_id) const {
    base::RunLoop().RunUntilIdle();
    return !MessageCenter::Get()->FindVisibleNotificationById(notification_id);
  }

  void DispatchGesture(const ui::GestureEventDetails& details) {
    ui::GestureEvent event2(0, 0, 0, ui::EventTimeForNow(), details);
    widget()->OnGestureEvent(&event2);
  }

  void BeginScroll() {
    DispatchGesture(
        ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
  }

  void EndScroll() {
    DispatchGesture(ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
  }

  void ScrollBy(int dx) {
    DispatchGesture(
        ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, dx, 0));
  }

  ArcNotificationContentView* content_view() {
    return notification_view_->content_view_;
  }

  views::View* collapsed_summary_view() {
    return notification_view_->collapsed_summary_view_;
  }

  bool IsGroupChild() { return notification_view_->is_group_child_; }

  views::Widget* widget() { return notification_view_->GetWidget(); }
  ArcNotificationView* notification_view() { return notification_view_; }

 protected:
  const std::string kDefaultNotificationKey = "notification_id";
  const std::string kDefaultNotificationId =
      kArcNotificationIdPrefix + kDefaultNotificationKey;

 private:
  std::unique_ptr<message_center::MessageView> CreateCustomMessageViewForTest(
      ArcNotificationItem* item,
      const Notification& notification,
      bool shown_in_popup) {
    auto message_view = std::make_unique<ArcNotificationView>(
        item, notification, shown_in_popup);
    message_view->content_view_->SetPreferredSize(gfx::Size(100, 100));
    return message_view;
  }

  std::unique_ptr<MockArcNotificationSurface> surface_;
  std::unique_ptr<Notification> notification_;
  raw_ptr<ArcNotificationView> notification_view_ =
      nullptr;  // owned by its widget.

  std::unique_ptr<MockArcNotificationItem> item_;
};

TEST_F(ArcNotificationViewTest, Events) {
  widget()->Show();

  gfx::Point cursor_location(1, 1);
  views::View::ConvertPointToWidget(content_view(), &cursor_location);
  EXPECT_EQ(content_view(),
            widget()->GetRootView()->GetEventHandlerForPoint(cursor_location));

  content_view()->RequestFocus();
  ui::KeyEvent key_event(ui::EventType::kKeyPressed, ui::VKEY_A, ui::EF_NONE);
  EXPECT_EQ(content_view(),
            static_cast<ui::EventTargeter*>(
                widget()->GetRootView()->GetEffectiveViewTargeter())
                ->FindTargetForEvent(widget()->GetRootView(), &key_event));
}

TEST_F(ArcNotificationViewTest, SlideOut) {
  ui::ScopedAnimationDurationScaleMode zero_duration_scope(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  std::string notification_id(kDefaultNotificationId);

  BeginScroll();
  EXPECT_EQ(0.f, GetNotificationSlideAmount());
  ScrollBy(-10);
  EXPECT_FALSE(IsPopupRemovedAfterIdle(notification_id));
  EXPECT_EQ(-10.f, GetNotificationSlideAmount());
  EndScroll();
  EXPECT_FALSE(IsPopupRemovedAfterIdle(notification_id));
  EXPECT_EQ(0.f, GetNotificationSlideAmount());

  BeginScroll();
  EXPECT_EQ(0.f, GetNotificationSlideAmount());
  ScrollBy(-200);
  EXPECT_FALSE(IsPopupRemovedAfterIdle(notification_id));
  EXPECT_EQ(-200.f, GetNotificationSlideAmount());
  EndScroll();
  EXPECT_TRUE(IsPopupRemovedAfterIdle(notification_id));
}

// TODO(crbug.com/40889858): Flaky on MSAN bots.
#if defined(MEMORY_SANITIZER)
#define MAYBE_SlideOutNested DISABLED_SlideOutNested
#else
#define MAYBE_SlideOutNested SlideOutNested
#endif
TEST_F(ArcNotificationViewTest, MAYBE_SlideOutNested) {
  ui::ScopedAnimationDurationScaleMode zero_duration_scope(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  notification_view()->SetIsNested();
  std::string notification_id(kDefaultNotificationId);

  BeginScroll();
  EXPECT_EQ(0.f, GetNotificationSlideAmount());
  ScrollBy(-10);
  EXPECT_FALSE(IsPopupRemovedAfterIdle(notification_id));
  EXPECT_EQ(-10.f, GetNotificationSlideAmount());
  EndScroll();
  EXPECT_FALSE(IsPopupRemovedAfterIdle(notification_id));
  EXPECT_EQ(0.f, GetNotificationSlideAmount());

  BeginScroll();
  EXPECT_EQ(0.f, GetNotificationSlideAmount());
  ScrollBy(-200);
  EXPECT_FALSE(IsPopupRemovedAfterIdle(notification_id));
  EXPECT_EQ(-200.f, GetNotificationSlideAmount());
  EndScroll();
  EXPECT_TRUE(IsPopupRemovedAfterIdle(notification_id));
}

TEST_F(ArcNotificationViewTest, SlideOutPinned) {
  ui::ScopedAnimationDurationScaleMode zero_duration_scope(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  std::unique_ptr<Notification> notification = CreateSimpleNotification();
  notification->set_pinned(true);
  notification_view()->SetIsNested();
  UpdateNotificationViews(*notification);
  std::string notification_id(kDefaultNotificationId);

  BeginScroll();
  EXPECT_EQ(0.f, GetNotificationSlideAmount());
  ScrollBy(-200);
  EXPECT_FALSE(IsRemovedAfterIdle(notification_id));
  EXPECT_LT(-200.f, GetNotificationSlideAmount());
  EndScroll();
  EXPECT_EQ(0.f, GetNotificationSlideAmount());
  EXPECT_FALSE(IsRemovedAfterIdle(notification_id));
}

TEST_F(ArcNotificationViewTest, SnoozeButton) {
  ui::ScopedAnimationDurationScaleMode zero_duration_scope(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  message_center::RichNotificationData rich_data;
  rich_data.pinned = true;
  rich_data.should_show_snooze_button = true;
  std::unique_ptr<Notification> notification = std::make_unique<Notification>(
      message_center::NOTIFICATION_TYPE_CUSTOM, kDefaultNotificationId,
      u"title", u"message", ui::ImageModel(), u"display source", GURL(),
      message_center::NotifierId(message_center::NotifierType::ARC_APPLICATION,
                                 "test_app_id"),
      rich_data, nullptr);

  UpdateNotificationViews(*notification);
  notification_view()->SetIsNested();

  EXPECT_NE(nullptr,
            notification_view()->GetControlButtonsView()->snooze_button());
}

TEST_F(ArcNotificationViewTest, PressBackspaceKey) {
  std::string notification_id(kDefaultNotificationId);
  content_view()->RequestFocus();

  ui::InputMethod* input_method = content_view()->GetInputMethod();
  ASSERT_TRUE(input_method);
  TestTextInputClient text_input_client;
  input_method->SetFocusedTextInputClient(&text_input_client);
  ASSERT_EQ(&text_input_client, input_method->GetTextInputClient());

  EXPECT_FALSE(IsRemovedAfterIdle(notification_id));
  PerformKeyEvents(ui::VKEY_BACK);
  EXPECT_TRUE(IsRemovedAfterIdle(notification_id));

  input_method->SetFocusedTextInputClient(nullptr);
}

TEST_F(ArcNotificationViewTest, PressBackspaceKeyOnEditBox) {
  std::string notification_id(kDefaultNotificationId);
  content_view()->RequestFocus();

  ui::InputMethod* input_method = content_view()->GetInputMethod();
  ASSERT_TRUE(input_method);
  TestTextInputClient text_input_client;
  input_method->SetFocusedTextInputClient(&text_input_client);
  ASSERT_EQ(&text_input_client, input_method->GetTextInputClient());

  text_input_client.set_text_input_type(ui::TEXT_INPUT_TYPE_TEXT);

  EXPECT_FALSE(IsRemovedAfterIdle(notification_id));
  PerformKeyEvents(ui::VKEY_BACK);
  EXPECT_FALSE(IsRemovedAfterIdle(notification_id));

  input_method->SetFocusedTextInputClient(nullptr);
}

TEST_F(ArcNotificationViewTest, ChangeContentHeight) {
  // Default size.
  gfx::Size size = notification_view()->GetPreferredSize();
  size.Enlarge(0, -notification_view()->GetInsets().height());
  EXPECT_EQ("100x100", size.ToString());

  // Allow small notifications.
  content_view()->SetPreferredSize(gfx::Size(10, 10));
  size = notification_view()->GetPreferredSize();
  size.Enlarge(0, -notification_view()->GetInsets().height());
  EXPECT_EQ("10x10", size.ToString());

  // The long notification.
  content_view()->SetPreferredSize(gfx::Size(1000, 1000));
  size = notification_view()->GetPreferredSize();
  size.Enlarge(0, -notification_view()->GetInsets().height());
  EXPECT_EQ("1000x1000", size.ToString());
}

TEST_F(ArcNotificationViewTest, TrackPadGestureSlideOut) {
  ui::ScopedAnimationDurationScaleMode zero_duration_scope(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  ui::test::EventGenerator generator(
      (notification_view()->GetWidget()->GetNativeWindow()->GetRootWindow()));
  generator.ScrollSequence(gfx::Point(), base::TimeDelta(), /*x_offset=*/200,
                           /*y_offset=*/0, /*steps=*/1, /*num_fingers=*/2);
  EXPECT_TRUE(IsPopupRemovedAfterIdle(kDefaultNotificationId));
}

class ArcNotificationViewRenderByChromeEnabledTest
    : public ArcNotificationViewTest {
 public:
  ArcNotificationViewRenderByChromeEnabledTest() = default;

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

  ~ArcNotificationViewRenderByChromeEnabledTest() override = default;

  // Overridden from ViewsTestBase:
  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(
        ash::features::kRenderArcNotificationsByChrome);

    ArcNotificationViewTest::SetUp();
  }

  // Check that smoothness should be recorded after an animation is performed on
  // a particular view.
  // This is copied from
  // ash/system/notification_center/views/ash_notification_view_unittest.cc.
  void CheckSmoothnessRecorded(base::HistogramTester& histograms,
                               views::View* view,
                               const char* animation_histogram_name,
                               int data_point_count = 1) {
    ui::Compositor* compositor = view->layer()->GetCompositor();

    ui::LayerAnimationStoppedWaiter animation_waiter;
    animation_waiter.Wait(view->layer());

    // Force frames and wait for all throughput trackers to be gone to allow
    // animation throughput data to be passed from cc to ui.
    while (compositor->has_throughput_trackers_for_testing()) {
      compositor->ScheduleFullRedraw();
      std::ignore = ui::WaitForNextFrameToBePresented(compositor,
                                                      base::Milliseconds(500));
    }

    // Smoothness should be recorded.
    histograms.ExpectTotalCount(animation_histogram_name, data_point_count);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// TODO(b/324991437)): the test is disabled due to recent flaky results.
TEST_F(ArcNotificationViewRenderByChromeEnabledTest,
       DISABLED_AnimateGroupedChildExpandedCollapseChanged) {
  // Enable animations.
  ui::ScopedAnimationDurationScaleMode duration(
      ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);

  std::unique_ptr<Notification> notification = CreateSimpleNotification();
  notification->SetGroupChild();
  UpdateNotificationViews(*notification);
  EXPECT_TRUE(IsGroupChild());
  EXPECT_NE(nullptr, collapsed_summary_view());

  // Expected histogram logged when expanding/collapsing.
  notification_view()->AnimateGroupedChildExpandedCollapse(true);

  base::HistogramTester tester_;
  CheckSmoothnessRecorded(
      tester_, collapsed_summary_view(),
      "Arc.NotificationView.CollapsedSummaryView.FadeOut.AnimationSmoothness");

  // Expected behavior in collapsed state.
  notification_view()->AnimateGroupedChildExpandedCollapse(false);

  CheckSmoothnessRecorded(
      tester_, collapsed_summary_view(),
      "Arc.NotificationView.CollapsedSummaryView.FadeIn.AnimationSmoothness");
}

TEST_F(ArcNotificationViewRenderByChromeEnabledTest,
       GroupedChildExpandStateChanged) {
  std::unique_ptr<Notification> notification = CreateSimpleNotification();
  notification->SetGroupChild();
  UpdateNotificationViews(*notification);
  EXPECT_TRUE(IsGroupChild());
  EXPECT_NE(nullptr, collapsed_summary_view());

  // Expected behavior in expanded state.
  notification_view()->SetGroupedChildExpanded(true);
  EXPECT_TRUE(content_view()->GetVisible());
  EXPECT_FALSE(collapsed_summary_view()->GetVisible());

  // Expected behavior in collapsed state.
  notification_view()->SetGroupedChildExpanded(false);
  EXPECT_FALSE(content_view()->GetVisible());
  EXPECT_TRUE(collapsed_summary_view()->GetVisible());
}

}  // namespace ash