chromium/ash/user_education/views/help_bubble_view_ash_unittest.cc

// Copyright 2023 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/user_education/views/help_bubble_view_ash.h"

#include <optional>
#include <vector>

#include "ash/public/cpp/shell_window_ids.h"
#include "ash/user_education/user_education_types.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/views/help_bubble_view_ash_test_base.h"
#include "ash/wm/window_util.h"
#include "components/user_education/common/help_bubble_params.h"
#include "components/vector_icons/vector_icons.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {
namespace {

// Aliases.
using ::testing::AnyOf;
using ::testing::Conditional;
using ::testing::Eq;
using ::testing::IsNull;
using ::testing::Not;
using ::testing::Property;
using ::user_education::HelpBubbleArrow;

// Matchers --------------------------------------------------------------------

MATCHER_P(Contains, window, "") {
  return arg->Contains(window);
}

// Helpers ---------------------------------------------------------------------

ui::MouseEvent CreateMouseMovedEvent(aura::Window* target,
                                     const gfx::Point& location_in_screen) {
  gfx::Point location(location_in_screen);
  wm::ConvertPointFromScreen(target, &location);
  ui::MouseEvent mouse_moved_event(ui::EventType::kMouseMoved, location,
                                   location_in_screen, ui::EventTimeForNow(),
                                   /*flags=*/ui::EF_NONE,
                                   /*changed_button_flags=*/ui::EF_NONE);
  ui::Event::DispatcherApi(&mouse_moved_event).set_target(target);
  return mouse_moved_event;
}

std::vector<gfx::Point> GetPerimeterPoints(const gfx::Rect& rect) {
  return std::vector<gfx::Point>({rect.origin(), rect.top_center(),
                                  rect.top_right(), rect.right_center(),
                                  rect.bottom_right(), rect.bottom_center(),
                                  rect.bottom_left(), rect.left_center()});
}

}  // namespace

// HelpBubbleViewAshTest -------------------------------------------------------

// Base class for tests of `HelpBubbleViewAsh`.
using HelpBubbleViewAshTest = HelpBubbleViewAshTestBase;

// Tests -----------------------------------------------------------------------

// Verifies that help bubbles are contained within the correct parent window.
TEST_F(HelpBubbleViewAshTest, ParentWindow) {
  auto* const help_bubble_view = CreateHelpBubbleView();
  EXPECT_TRUE(help_bubble_view->anchor_widget()
                  ->GetNativeWindow()
                  ->GetRootWindow()
                  ->GetChildById(kShellWindowId_HelpBubbleContainer)
                  ->Contains(help_bubble_view->GetWidget()->GetNativeWindow()));
}

// HelpBubbleViewAshBodyIconTest -----------------------------------------------

// Base class for tests of `HelpBubbleViewAsh` which are primarily concerned
// with body icons, parameterized by:
// (a) the body icon from help bubble params, and
// (b) the body icon from extended properties.
class HelpBubbleViewAshBodyIconTest
    : public HelpBubbleViewAshTestBase,
      public ::testing::WithParamInterface<
          std::tuple</*body_icon_from_params=*/std::optional<
                         std::reference_wrapper<const gfx::VectorIcon>>,
                     /*body_icon_from_extended_properties=*/std::optional<
                         std::reference_wrapper<const gfx::VectorIcon>>>> {
 public:
  // Returns the body icon from help bubble params given test parameterization.
  const std::optional<std::reference_wrapper<const gfx::VectorIcon>>
  body_icon_from_params() const {
    return std::get<0>(GetParam());
  }

  // Returns the body icon from extended properties given test parameterization.
  const std::optional<std::reference_wrapper<const gfx::VectorIcon>>
  body_icon_from_extended_properties() const {
    return std::get<1>(GetParam());
  }
};

INSTANTIATE_TEST_SUITE_P(
    All,
    HelpBubbleViewAshBodyIconTest,
    ::testing::Combine(
        /*body_icon_from_params=*/::testing::Values(
            std::make_optional(std::cref(gfx::kNoneIcon)),
            std::make_optional(std::cref(vector_icons::kCelebrationIcon)),
            std::make_optional(std::cref(vector_icons::kHelpIcon)),
            std::nullopt),
        /*body_icon_from_extended_properties=*/::testing::Values(
            std::make_optional(std::cref(gfx::kNoneIcon)),
            std::make_optional(std::cref(vector_icons::kCelebrationIcon)),
            std::make_optional(std::cref(vector_icons::kHelpIcon)),
            std::nullopt)));

// Tests -----------------------------------------------------------------------

// Verifies that help bubble view body icons are configured as expected.
TEST_P(HelpBubbleViewAshBodyIconTest, BodyIcon) {
  // Construct help bubble `params`.
  user_education::HelpBubbleParams params;
  params.extended_properties =
      user_education_util::CreateExtendedProperties(HelpBubbleId::kTest);

  // Populate `body_icon_from_params` based on parameterization.
  if (const auto& body_icon_from_params = this->body_icon_from_params()) {
    params.body_icon = &body_icon_from_params->get();
  }

  // Populate `body_icon_from_extended_properties` based on parameterization.
  if (const auto& body_icon_from_extended_properties =
          this->body_icon_from_extended_properties()) {
    params.extended_properties = user_education_util::CreateExtendedProperties(
        std::move(params.extended_properties),
        user_education_util::CreateExtendedProperties(
            body_icon_from_extended_properties->get()));
  }

  // Create `help_bubble_view`.
  auto* const help_bubble_view = CreateHelpBubbleView(std::move(params));
  ASSERT_NE(help_bubble_view, nullptr);

  // Cache `expected_body_icon` based on order of precedence.
  const gfx::VectorIcon& expected_body_icon =
      body_icon_from_extended_properties().value_or(
          body_icon_from_params().value_or(gfx::kNoneIcon));

  // Confirm body icon exists iff expected and is configured as expected.
  EXPECT_THAT(
      views::ElementTrackerViews::GetInstance()
          ->GetUniqueViewAs<views::ImageView>(
              HelpBubbleViewAsh::kBodyIconIdForTesting,
              views::ElementTrackerViews::GetContextForView(help_bubble_view)),
      Conditional(&expected_body_icon != &gfx::kNoneIcon,
                  Property(&views::ImageView::GetImageModel,
                           Eq(ui::ImageModel::FromVectorIcon(
                               expected_body_icon,
                               cros_tokens::kCrosSysDialogContainer, 20))),
                  IsNull()));
}

// Verifies that help bubbles have the appropriate background color.
TEST_F(HelpBubbleViewAshTest, BackgroundColor) {
  const auto* const help_bubble_view = CreateHelpBubbleView();
  const auto* const color_provider = help_bubble_view->GetColorProvider();
  EXPECT_EQ(help_bubble_view->color(),
            color_provider->GetColor(cros_tokens::kCrosSysDialogContainer));
}

// Verifies that help bubbles can activate.
TEST_F(HelpBubbleViewAshTest, CanActivate) {
  const auto* const help_bubble_view = CreateHelpBubbleView();
  EXPECT_TRUE(help_bubble_view->CanActivate());
}

// Verifies that help bubbles do not handle events within their shadows.
TEST_F(HelpBubbleViewAshTest, HitTest) {
  auto* const help_bubble_view = CreateHelpBubbleView();
  auto* const help_bubble_widget = help_bubble_view->GetWidget();
  auto* const help_bubble_window = help_bubble_widget->GetNativeWindow();
  auto* const root_window = help_bubble_window->GetRootWindow();
  auto* const root_window_targeter = root_window->targeter();

  // Case: Event within help bubble contents bounds.
  // NOTE: We inset `contents_bounds` to account for fractional pixel rounding
  // in event processing. This ensures the event is within `contents_bounds`.
  gfx::Rect contents_bounds(help_bubble_view->GetBoundsInScreen());
  contents_bounds.Inset(1);

  // Events within help bubble `contents_bounds` should be handled.
  for (const gfx::Point& point : GetPerimeterPoints(contents_bounds)) {
    EXPECT_THAT(window_util::GetEventHandlerForEvent(
                    CreateMouseMovedEvent(root_window, point)),
                AnyOf(Eq(help_bubble_window), Contains(help_bubble_window)));

    // Confirm that the help bubble window will be targeted for the event so its
    // events don't leak through to windows behind it.
    // TODO(http://b/307780200): Possibly remove this when `WindowTargeter` is
    // updated, since it should be tested at that level
    ui::MouseEvent press(ui::EventType::kMousePressed, point, point,
                         base::TimeTicks::Now(), ui::EF_NONE,
                         ui::EF_LEFT_MOUSE_BUTTON);
    EXPECT_EQ(root_window_targeter->FindTargetForEvent(root_window, &press),
              help_bubble_window);
  }

  // Case: Event within help bubble shadow bounds.
  // NOTE: We outset contents bounds to enlarge the rect into the shadow.
  gfx::Rect shadow_bounds(help_bubble_view->GetBoundsInScreen());
  shadow_bounds.Outset(1);
  // Events within help bubble `shadow_bounds` should *not* be handled, nor
  // should they target that window.
  for (const gfx::Point& point : GetPerimeterPoints(shadow_bounds)) {
    EXPECT_THAT(
        window_util::GetEventHandlerForEvent(
            CreateMouseMovedEvent(root_window, point)),
        Not(AnyOf(Eq(help_bubble_window), Contains(help_bubble_window))));

    // Also confirm that the help bubble window will not be targeted for the
    // event so it doesn't block events in its shadow.
    // TODO(http://b/307780200): Possibly remove this when `WindowTargeter` is
    // updated, since it should be tested at that level
    ui::MouseEvent press(ui::EventType::kMousePressed, point, point,
                         base::TimeTicks::Now(), ui::EF_NONE,
                         ui::EF_LEFT_MOUSE_BUTTON);
    EXPECT_NE(root_window_targeter->FindTargetForEvent(root_window, &press),
              help_bubble_window);
  }
}

}  // namespace ash