[go: nahoru, domu]

blob: 44d18ae36680185a54af3e3c70a8dc09717f5181 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/user_education/views/help_bubble_view.h"
#include <memory>
#include <vector>
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/test/mock_callback.h"
#include "components/user_education/common/feature_promo_specification.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/help_bubble_params.h"
#include "components/user_education/views/help_bubble_delegate.h"
#include "components/user_education/views/help_bubble_factory_views.h"
#include "components/user_education/views/help_bubble_views_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_test_util.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
namespace user_education {
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kTestElementId);
const ui::ElementContext kTestElementContext{1};
constexpr gfx::Rect kWidgetBounds{400, 200, 200, 200};
} // namespace
// Unit tests for HelpBubbleView. Timeout functionality isn't tested here due to
// the vagaries of trying to get simulated timed events to run without a full
// execution environment (specifically, Mac tests were extremely flaky without
// the browser).
//
// Timeouts are tested in:
// chrome/browser/ui/views/user_education/help_bubble_view_timeout_unittest.cc
class HelpBubbleViewTest : public views::ViewsTestBase {
public:
HelpBubbleViewTest() = default;
~HelpBubbleViewTest() override = default;
void SetUp() override {
ViewsTestBase::SetUp();
widget_ = std::make_unique<test::TestThemedWidget>();
widget_->Init(CreateParamsForTestWidget());
view_ = widget_->SetContentsView(std::make_unique<views::View>());
widget_->Show();
widget_->SetBounds(kWidgetBounds);
}
void TearDown() override {
widget_.reset();
ViewsTestBase::TearDown();
}
protected:
gfx::Rect GetWidgetClientBounds() const {
return widget_->GetClientAreaBoundsInScreen();
}
HelpBubbleView* CreateHelpBubbleView(
HelpBubbleParams params,
absl::optional<gfx::Rect> bounds = absl::nullopt,
absl::optional<views::View*> view = absl::nullopt) {
internal::HelpBubbleAnchorParams anchor_params;
anchor_params.view = view.value_or(view_);
anchor_params.rect = bounds;
return new HelpBubbleView(&test_delegate_, anchor_params,
std::move(params));
}
HelpBubbleView* CreateHelpBubbleView(base::RepeatingClosure button_callback) {
HelpBubbleParams params;
params.body_text = u"To X, do Y";
params.arrow = HelpBubbleArrow::kTopRight;
if (button_callback) {
HelpBubbleButtonParams button_params;
button_params.text = u"Go away";
button_params.is_default = true;
button_params.callback = std::move(button_callback);
params.buttons.push_back(std::move(button_params));
}
return CreateHelpBubbleView(std::move(params));
}
test::TestHelpBubbleDelegate test_delegate_;
base::raw_ptr<views::View> view_;
std::unique_ptr<views::Widget> widget_;
};
TEST_F(HelpBubbleViewTest, CallButtonCallback_Mouse) {
UNCALLED_MOCK_CALLBACK(base::RepeatingClosure, mock_callback);
HelpBubbleView* const bubble = CreateHelpBubbleView(mock_callback.Get());
// Simulate clicks on dismiss button.
EXPECT_CALL_IN_SCOPE(
mock_callback, Run,
views::test::InteractionTestUtilSimulatorViews::PressButton(
bubble->GetDefaultButtonForTesting(),
ui::test::InteractionTestUtil::InputType::kMouse));
bubble->GetWidget()->Close();
}
TEST_F(HelpBubbleViewTest, CallButtonCallback_Keyboard) {
UNCALLED_MOCK_CALLBACK(base::RepeatingClosure, mock_callback);
HelpBubbleView* const bubble = CreateHelpBubbleView(mock_callback.Get());
// Simulate clicks on dismiss button.
EXPECT_CALL_IN_SCOPE(
mock_callback, Run,
views::test::InteractionTestUtilSimulatorViews::PressButton(
bubble->GetDefaultButtonForTesting(),
ui::test::InteractionTestUtil::InputType::kKeyboard));
bubble->GetWidget()->Close();
}
TEST_F(HelpBubbleViewTest, StableButtonOrder) {
HelpBubbleParams params;
params.body_text = u"To X, do Y";
params.arrow = HelpBubbleArrow::kTopRight;
constexpr char16_t kButton1Text[] = u"button 1";
constexpr char16_t kButton2Text[] = u"button 2";
constexpr char16_t kButton3Text[] = u"button 3";
HelpBubbleButtonParams button1;
button1.text = kButton1Text;
button1.is_default = false;
params.buttons.push_back(std::move(button1));
HelpBubbleButtonParams button2;
button2.text = kButton2Text;
button2.is_default = true;
params.buttons.push_back(std::move(button2));
HelpBubbleButtonParams button3;
button3.text = kButton3Text;
button3.is_default = false;
params.buttons.push_back(std::move(button3));
auto* bubble = new HelpBubbleView(&test_delegate_,
internal::HelpBubbleAnchorParams{view_},
std::move(params));
EXPECT_EQ(kButton1Text, bubble->GetNonDefaultButtonForTesting(0)->GetText());
EXPECT_EQ(kButton2Text, bubble->GetDefaultButtonForTesting()->GetText());
EXPECT_EQ(kButton3Text, bubble->GetNonDefaultButtonForTesting(1)->GetText());
}
TEST_F(HelpBubbleViewTest, AnchorToRect) {
HelpBubbleParams params;
params.body_text = u"To X, do Y";
params.arrow = HelpBubbleArrow::kRightCenter;
const auto widget_bounds = GetWidgetClientBounds();
gfx::Rect anchor_bounds = widget_bounds;
anchor_bounds.Inset(50);
HelpBubbleView* const bubble =
CreateHelpBubbleView(std::move(params), anchor_bounds);
const auto bubble_bounds = bubble->GetWidget()->GetWindowBoundsInScreen();
// The right side of the bubble should overlap the widget.
EXPECT_TRUE(widget_bounds.Contains(bubble_bounds.right_center()));
// The right side of the widget should be outside and aligned with the center
// of the anchor bounds. Allow for rounding error when checking alignment.
EXPECT_LT(bubble_bounds.right(), anchor_bounds.x());
EXPECT_LE(std::abs(bubble_bounds.CenterPoint().y() -
anchor_bounds.CenterPoint().y()),
2);
}
TEST_F(HelpBubbleViewTest, AnchorRectUpdated) {
HelpBubbleParams params;
params.body_text = u"To X, do Y";
params.arrow = HelpBubbleArrow::kRightCenter;
const auto widget_bounds = GetWidgetClientBounds();
gfx::Rect anchor_bounds = widget_bounds;
anchor_bounds.Inset(50);
HelpBubbleView* const bubble =
CreateHelpBubbleView(std::move(params), anchor_bounds);
const auto bubble_bounds = bubble->GetWidget()->GetWindowBoundsInScreen();
constexpr gfx::Vector2d kAnchorOffset{9, 13};
anchor_bounds.Offset(kAnchorOffset);
bubble->SetForceAnchorRect(anchor_bounds);
bubble->OnAnchorBoundsChanged();
gfx::Rect expected = bubble_bounds;
expected.Offset(kAnchorOffset);
EXPECT_EQ(expected, bubble->GetWidget()->GetWindowBoundsInScreen());
}
TEST_F(HelpBubbleViewTest, ScrollAnchorViewToVisible) {
views::ScrollView* scroll_view = nullptr;
views::View* anchor_view = nullptr;
// Add an `anchor_view` to the `view_` hierarchy that is hosted within a
// `scroll_view` and is initially outside the viewport.
views::Builder<views::View>(view_)
.SetUseDefaultFillLayout(true)
.AddChildren(
views::Builder<views::ScrollView>()
.CopyAddressTo(&scroll_view)
.ClipHeightTo(/*min_height=*/0, /*max_height=*/view_->height())
.SetContents(
views::Builder<views::FlexLayoutView>()
.SetCrossAxisAlignment(views::LayoutAlignment::kEnd)
.SetMainAxisAlignment(views::LayoutAlignment::kEnd)
.SetOrientation(views::LayoutOrientation::kVertical)
.AddChildren(
views::Builder<views::View>().SetPreferredSize(
view_->size()),
views::Builder<views::View>()
.CopyAddressTo(&anchor_view)
.SetPreferredSize(gfx::Size(10, 10)))))
.BuildChildren();
// Ensure `widget_` has finished processing the layout changes.
views::test::RunScheduledLayout(widget_.get());
// Initially `anchor_view` should not be visible.
EXPECT_FALSE(scroll_view->GetBoundsInScreen().Contains(
anchor_view->GetBoundsInScreen()));
// Expect that `scroll_view` will scroll when creating a help bubble anchored
// to `anchor_view` since it is outside the viewport.
base::MockRepeatingClosure callback;
EXPECT_CALL(callback, Run()).Times(testing::AtLeast(1));
base::CallbackListSubscription subscription =
scroll_view->AddContentsScrolledCallback(callback.Get());
// Create the help bubble anchored to `anchor_view`.
HelpBubbleParams params;
params.body_text = u"To X, do Y";
params.arrow = HelpBubbleArrow::kTopRight;
CreateHelpBubbleView(std::move(params), /*bounds=*/absl::nullopt,
anchor_view);
// Expect that `anchor_view` is now visible.
EXPECT_TRUE(scroll_view->GetBoundsInScreen().Contains(
anchor_view->GetBoundsInScreen()));
}
class HelpBubbleViewsTest : public HelpBubbleViewTest {
public:
HelpBubbleViewsTest() = default;
~HelpBubbleViewsTest() override = default;
// This simulates logic used by e.g. FloatingWebUIHelpBubbleFactory.
std::unique_ptr<HelpBubbleViews> CreateHelpBubble(
HelpBubbleParams params,
ui::TrackedElement* element) {
HelpBubbleView* const bubble_view =
CreateHelpBubbleView(std::move(params), element->GetScreenBounds());
return base::WrapUnique(new HelpBubbleViews(bubble_view, element));
}
void SetUp() override {
HelpBubbleViewTest::SetUp();
HelpBubbleParams params;
params.body_text = u"To X, do Y";
params.arrow = HelpBubbleArrow::kRightCenter;
gfx::Rect anchor_bounds = GetWidgetClientBounds();
anchor_bounds.Inset(50);
test_element_ = std::make_unique<ui::test::TestElement>(
kTestElementId, kTestElementContext);
test_element_->SetScreenBounds(anchor_bounds);
test_element_->Show();
help_bubble_ = CreateHelpBubble(std::move(params), test_element_.get());
}
void TearDown() override {
test_element_.reset();
HelpBubbleViewTest::TearDown();
}
protected:
gfx::Rect GetHelpBubbleAnchorRect() const {
return help_bubble_->bubble_view()->GetAnchorRect();
}
std::unique_ptr<ui::test::TestElement> test_element_;
std::unique_ptr<HelpBubbleViews> help_bubble_;
};
// This duplicates the HelpBubbleViewTest, but with a HelpBubbleViews object.
TEST_F(HelpBubbleViewsTest, AnchorToRect) {
const auto widget_bounds = GetWidgetClientBounds();
const auto anchor_bounds = test_element_->GetScreenBounds();
const auto bubble_bounds = help_bubble_->GetBoundsInScreen();
// The right side of the bubble should overlap the widget.
EXPECT_TRUE(widget_bounds.Contains(bubble_bounds.right_center()));
// The right side of the widget should be outside and aligned with the center
// of the anchor bounds. Allow for rounding error when checking alignment.
EXPECT_LT(bubble_bounds.right(), anchor_bounds.x());
EXPECT_LE(std::abs(bubble_bounds.CenterPoint().y() -
anchor_bounds.CenterPoint().y()),
2);
}
// This duplicates the HelpBubbleViewTest, but with a HelpBubbleViews object.
TEST_F(HelpBubbleViewsTest, AnchorRectUpdated) {
const gfx::Rect old_bounds = help_bubble_->GetBoundsInScreen();
// Move the anchor target by a small but noticeable amount.
auto new_bounds = test_element_->GetScreenBounds();
constexpr gfx::Vector2d kAnchorOffset{9, 13};
new_bounds.Offset(kAnchorOffset);
test_element_->SetScreenBounds(new_bounds);
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
test_element_.get(), kHelpBubbleAnchorBoundsChangedEvent);
// Verify that the help bubble has moved by a similar amount.
gfx::Rect expected = old_bounds;
expected.Offset(kAnchorOffset);
EXPECT_EQ(expected, help_bubble_->GetBoundsInScreen());
}
// This checks a case where the target anchor region scrolls partially out of
// the host view. The anchor rect should be the intersection of the two.
TEST_F(HelpBubbleViewsTest, AnchorRectOverlapsEdge) {
const gfx::Rect old_bounds = help_bubble_->GetBoundsInScreen();
// Move the anchor target so that the upper left is beyond the edge of the
// anchor view.
auto new_bounds = test_element_->GetScreenBounds();
new_bounds.Offset(-100, -100);
test_element_->SetScreenBounds(new_bounds);
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
test_element_.get(), kHelpBubbleAnchorBoundsChangedEvent);
// Verify that the help bubble has moved.
constexpr gfx::Rect kNewAnchorBounds{kWidgetBounds.x(), kWidgetBounds.y(), 50,
50};
EXPECT_EQ(kNewAnchorBounds, GetHelpBubbleAnchorRect());
const gfx::Rect help_bubble_bounds = help_bubble_->GetBoundsInScreen();
EXPECT_LT(help_bubble_bounds.y(), old_bounds.y());
EXPECT_GT(help_bubble_bounds.CenterPoint().y(), kNewAnchorBounds.y());
EXPECT_LT(help_bubble_bounds.CenterPoint().y(), kNewAnchorBounds.bottom());
// Bubble may have mirrored horizontally. Check which orientation it's in and
// verify the position is appropriate to the new anchor region.
switch (help_bubble_->bubble_view()->GetBubbleFrameView()->GetArrow()) {
case views::BubbleBorder::RIGHT_CENTER:
EXPECT_LT(help_bubble_bounds.x(), old_bounds.x());
EXPECT_LT(help_bubble_bounds.right(), kNewAnchorBounds.x());
break;
case views::BubbleBorder::LEFT_CENTER:
EXPECT_GT(help_bubble_bounds.x(), old_bounds.x());
EXPECT_GT(help_bubble_bounds.x(), kNewAnchorBounds.right());
break;
default:
NOTREACHED() << "Arrow should only be right-center or left-center.";
}
}
// This checks a case where the target anchor region scrolls fully out of
// the host view. The anchor rect should be a one-pixel slice on the edge
// closest to the actual anchor.
TEST_F(HelpBubbleViewsTest, AnchorOutsideBoundsHorizontal) {
const gfx::Rect old_bounds = help_bubble_->GetBoundsInScreen();
// Move the anchor target entirely off the right side of the anchor view.
auto new_bounds = test_element_->GetScreenBounds();
new_bounds.Offset(200, 0);
test_element_->SetScreenBounds(new_bounds);
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
test_element_.get(), kHelpBubbleAnchorBoundsChangedEvent);
// Verify that the help bubble has moved. It might be mirrored, however.
constexpr gfx::Rect kNewAnchorBounds{kWidgetBounds.right() - 1,
kWidgetBounds.y() + 50, 1, 100};
EXPECT_EQ(kNewAnchorBounds, GetHelpBubbleAnchorRect());
const gfx::Rect help_bubble_bounds = help_bubble_->GetBoundsInScreen();
EXPECT_EQ(help_bubble_bounds.y(), old_bounds.y());
EXPECT_GT(help_bubble_bounds.x(), old_bounds.x());
EXPECT_LT(help_bubble_bounds.right(), kNewAnchorBounds.x());
}
// This checks a case where the target anchor region scrolls fully out of
// the host view. The anchor rect should be a one-pixel slice on the edge
// closest to the actual anchor.
TEST_F(HelpBubbleViewsTest, AnchorOutsideBoundsVertical) {
const gfx::Rect old_bounds = help_bubble_->GetBoundsInScreen();
// Move the anchor target entirely beyond the bottom of the anchor view.
auto new_bounds = test_element_->GetScreenBounds();
new_bounds.Offset(0, 200);
test_element_->SetScreenBounds(new_bounds);
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
test_element_.get(), kHelpBubbleAnchorBoundsChangedEvent);
// Verify that the help bubble has moved. It might be mirrored, however.
constexpr gfx::Rect kNewAnchorBounds{kWidgetBounds.x() + 50,
kWidgetBounds.bottom() - 1, 100, 1};
EXPECT_EQ(kNewAnchorBounds, GetHelpBubbleAnchorRect());
const gfx::Rect help_bubble_bounds = help_bubble_->GetBoundsInScreen();
EXPECT_EQ(help_bubble_bounds.x(), old_bounds.x());
EXPECT_GT(help_bubble_bounds.y(), old_bounds.y());
EXPECT_LT(help_bubble_bounds.y(), kNewAnchorBounds.y());
EXPECT_GE(help_bubble_bounds.bottom(), kNewAnchorBounds.y());
EXPECT_LT(help_bubble_bounds.right(), kNewAnchorBounds.x());
}
// Verifies that a bubble anchored to a region will still move with the owning
// Widget.
TEST_F(HelpBubbleViewsTest, MoveAnchorWidget) {
const auto old_bubble_bounds = help_bubble_->GetBoundsInScreen();
gfx::Rect widget_bounds = widget_->GetWindowBoundsInScreen();
constexpr gfx::Vector2d kOffset{9, 13};
widget_bounds.Offset(kOffset);
widget_->SetBounds(widget_bounds);
gfx::Rect expected = old_bubble_bounds;
expected.Offset(kOffset);
EXPECT_EQ(expected, help_bubble_->GetBoundsInScreen());
}
} // namespace user_education