Skip to content

Commit

Permalink
Create subscriber APIs for help bubble events.
Browse files Browse the repository at this point in the history
This CL exposes the ability to subscribe to the following events
through the `UserEducationHelpBubbleController`:

* Help bubble anchor bounds changed
* Help bubble closed
* Help bubble shown

These events will be used to invalidate the Welcome Tour scrim which
will need to mask cut outs for help bubble anchor views.

Note that this CL uses CallbackLists rather than Observers to be as
consistent as possible with the ElementTracker infrastructure upon
which user education services are built:

https://source.chromium.org/chromium/chromium/src/+/main:ui/base/interaction/element_tracker.h;l=164-195

Bug: b:277091650
Change-Id: Ie94546103bcaac166cf3ac820356ff2d3cc2fbb4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4581813
Commit-Queue: David Black <dmblack@google.com>
Reviewed-by: Andrew Xu <andrewxu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1153680}
  • Loading branch information
David Black authored and Chromium LUCI CQ committed Jun 6, 2023
1 parent ba0a222 commit 7f72814
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 49 deletions.
34 changes: 34 additions & 0 deletions ash/user_education/user_education_help_bubble_controller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,38 @@ absl::optional<HelpBubbleId> UserEducationHelpBubbleController::GetHelpBubbleId(
return absl::nullopt;
}

base::CallbackListSubscription
UserEducationHelpBubbleController::AddHelpBubbleAnchorBoundsChangedCallback(
base::RepeatingClosure callback) {
return help_bubble_anchor_bounds_changed_subscribers_.Add(
std::move(callback));
}

base::CallbackListSubscription
UserEducationHelpBubbleController::AddHelpBubbleClosedCallback(
base::RepeatingClosure callback) {
return help_bubble_closed_subscribers_.Add(std::move(callback));
}

base::CallbackListSubscription
UserEducationHelpBubbleController::AddHelpBubbleShownCallback(
base::RepeatingClosure callback) {
return help_bubble_shown_subscribers_.Add(std::move(callback));
}

void UserEducationHelpBubbleController::NotifyHelpBubbleAnchorBoundsChanged(
base::PassKey<HelpBubbleViewAsh>) {
help_bubble_anchor_bounds_changed_subscribers_.Notify();
}

void UserEducationHelpBubbleController::NotifyHelpBubbleClosed(
base::PassKey<HelpBubbleViewAsh>) {
help_bubble_closed_subscribers_.Notify();
}

void UserEducationHelpBubbleController::NotifyHelpBubbleShown(
base::PassKey<HelpBubbleViewAsh>) {
help_bubble_shown_subscribers_.Notify();
}

} // namespace ash
34 changes: 34 additions & 0 deletions ash/user_education/user_education_help_bubble_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "base/callback_list.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/types/pass_key.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace ui {
Expand All @@ -25,6 +26,7 @@ struct HelpBubbleParams;

namespace ash {

class HelpBubbleViewAsh;
class UserEducationDelegate;
enum class HelpBubbleId;

Expand Down Expand Up @@ -65,6 +67,30 @@ class ASH_EXPORT UserEducationHelpBubbleController {
ui::ElementIdentifier element_id,
ui::ElementContext element_context) const;

// Adds a `callback` to be invoked whenever a help bubble's anchor bounds
// change until the returned subscription is destroyed.
[[nodiscard]] base::CallbackListSubscription
AddHelpBubbleAnchorBoundsChangedCallback(base::RepeatingClosure callback);

// Adds a `callback` to be invoked whenever a help bubble is closed until the
// returned subscription is destroyed.
[[nodiscard]] base::CallbackListSubscription AddHelpBubbleClosedCallback(
base::RepeatingClosure callback);

// Adds a `callback` to be invoked whenever a help bubble is shown until the
// returned subscription is destroyed.
[[nodiscard]] base::CallbackListSubscription AddHelpBubbleShownCallback(
base::RepeatingClosure callback);

// Invoked by `HelpBubbleViewAsh` when a help bubble's anchor bounds change.
void NotifyHelpBubbleAnchorBoundsChanged(base::PassKey<HelpBubbleViewAsh>);

// Invoked by `HelpBubbleViewAsh` when a help bubble is closed.
void NotifyHelpBubbleClosed(base::PassKey<HelpBubbleViewAsh>);

// Invoked by `HelpBubbleViewAsh` when a help bubble is shown.
void NotifyHelpBubbleShown(base::PassKey<HelpBubbleViewAsh>);

private:
// The delegate owned by the `UserEducationController` which facilitates
// communication between Ash and user education services in the browser.
Expand All @@ -74,6 +100,14 @@ class ASH_EXPORT UserEducationHelpBubbleController {
// notified when it closes. Once closed, help bubble related memory is freed.
std::unique_ptr<user_education::HelpBubble> help_bubble_;
base::CallbackListSubscription help_bubble_close_subscription_;

// Lists of subscribers to notify for the following events:
// (a) Help bubble anchor bounds changed
// (b) Help bubble closed
// (c) Help bubble shown
base::RepeatingClosureList help_bubble_anchor_bounds_changed_subscribers_;
base::RepeatingClosureList help_bubble_closed_subscribers_;
base::RepeatingClosureList help_bubble_shown_subscribers_;
};

} // namespace ash
Expand Down
194 changes: 146 additions & 48 deletions ash/user_education/user_education_help_bubble_controller_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
#include "ash/user_education/user_education_help_bubble_controller.h"

#include <memory>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/user_education/mock_user_education_delegate.h"
#include "ash/user_education/user_education_ash_test_base.h"
#include "ash/user_education/user_education_types.h"
#include "ash/user_education/user_education_util.h"
#include "ash/user_education/views/help_bubble_factory_views_ash.h"
#include "base/callback_list.h"
#include "base/test/mock_callback.h"
#include "base/test/repeating_test_future.h"
#include "base/test/scoped_feature_list.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/help_bubble_params.h"
Expand Down Expand Up @@ -45,6 +48,21 @@ using ::user_education::HelpBubbleParams;
// Element identifiers.
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kElementId);

// Actions ---------------------------------------------------------------------

// NOTE: This action intentionally does *not* use `ACTION_P` macros, as actions
// generated in that way struggle to support move-only types.
template <typename ClassPtr, typename MethodPtr, typename ResultPtr>
auto InvokeAndCopyResultAddressTo(ClassPtr class_ptr,
MethodPtr method_ptr,
ResultPtr output_ptr) {
return [class_ptr, method_ptr, output_ptr](auto&&... args) {
auto result = (class_ptr->*method_ptr)(std::move(args)...);
*output_ptr = result.get();
return result;
};
}

} // namespace

// UserEducationHelpBubbleControllerTest ---------------------------------------
Expand All @@ -63,25 +81,86 @@ class UserEducationHelpBubbleControllerTest : public UserEducationAshTestBase {
scoped_feature_list_.InitWithFeatures(enabled_features, {});
}

// Creates and returns a help bubble for the specified `help_bubble_params`,
// anchored to the `help_bubble_anchor_widget()`.
std::unique_ptr<HelpBubble> CreateHelpBubble(
HelpBubbleParams help_bubble_params) {
// Set `help_bubble_id` in extended properties.
help_bubble_params.extended_properties.values().Merge(std::move(
user_education_util::CreateExtendedProperties(HelpBubbleId::kTest)
.values()));

// Create the help bubble.
return help_bubble_factory()->CreateBubble(
ui::ElementTracker::GetElementTracker()->GetFirstMatchingElement(
kElementId, help_bubble_anchor_context()),
std::move(help_bubble_params));
}

// Returns the singleton instance owned by the `UserEducationController`.
UserEducationHelpBubbleController* controller() {
return UserEducationHelpBubbleController::Get();
}

// Returns the element context to use for help bubble anchors.
ui::ElementContext help_bubble_anchor_context() const {
return views::ElementTrackerViews::GetContextForWidget(
help_bubble_anchor_widget_.get());
}

// Returns the widget to use for help bubble anchors.
views::Widget* help_bubble_anchor_widget() {
return help_bubble_anchor_widget_.get();
}

// Returns the factory to use to create help bubbles.
HelpBubbleFactoryViewsAsh* help_bubble_factory() {
return &help_bubble_factory_;
}

// Returns the account ID for the primary user profile which is logged in
// during test `SetUp()`. Note that user education in Ash is currently only
// supported for the primary user profile.
const AccountId& primary_user_account_id() const {
return primary_user_account_id_;
}

private:
// UserEducationAshTestBase:
void SetUp() override {
UserEducationAshTestBase::SetUp();

// User education in Ash is currently only supported for the primary user
// profile. This is a self-imposed restriction. Log in the primary user.
primary_user_account_id_ = AccountId::FromUserEmail("primary@test");
SimulateUserLogin(primary_user_account_id_);

// Create and show a `help_bubble_anchor_widget_`.
help_bubble_anchor_widget_ = CreateFramelessTestWidget();
help_bubble_anchor_widget_->SetContentsView(
views::Builder<views::View>()
.SetProperty(views::kElementIdentifierKey, kElementId)
.Build());
help_bubble_anchor_widget_->CenterWindow(gfx::Size(50, 50));
help_bubble_anchor_widget_->ShowInactive();
}

// Used to enable user education features which are required for existence of
// the `controller()` under test.
base::test::ScopedFeatureList scoped_feature_list_;

// The widget to use for help bubble anchors.
views::UniqueWidgetPtr help_bubble_anchor_widget_;

// Used to mock help bubble creation given that user education services in
// the browser are non-existent for unit tests in Ash.
user_education::test::TestHelpBubbleDelegate help_bubble_delegate_;
HelpBubbleFactoryViewsAsh help_bubble_factory_{&help_bubble_delegate_};

// The account ID for the primary user profile which is logged in during test
// `SetUp()`. Note that user education in Ash is currently only supported for
// the primary user profile.
AccountId primary_user_account_id_;
};

// Tests -----------------------------------------------------------------------
Expand All @@ -90,28 +169,13 @@ class UserEducationHelpBubbleControllerTest : public UserEducationAshTestBase {
// tracked element, and that `GetHelpBubbleId()` can be used to retrieve the ID
// of the currently showing help bubble for a tracked element.
TEST_F(UserEducationHelpBubbleControllerTest, CreateHelpBubble) {
// User education in Ash is currently only supported for the primary user
// profile. This is a self-imposed restriction. Log in the primary user.
AccountId primary_user_account_id = AccountId::FromUserEmail("primary@test");
SimulateUserLogin(primary_user_account_id);

// Create and show a `widget` to serve as help bubble anchor.
views::UniqueWidgetPtr widget = CreateFramelessTestWidget();
widget->SetContentsView(
views::Builder<views::View>()
.SetProperty(views::kElementIdentifierKey, kElementId)
.Build());
widget->CenterWindow(gfx::Size(50, 50));
widget->ShowInactive();

// Cache `element_context`.
const ui::ElementContext element_context =
views::ElementTrackerViews::GetContextForWidget(widget.get());
// Cache the `element_context` to use for help bubble anchors.
const ui::ElementContext element_context = help_bubble_anchor_context();

// Help bubble creation is delegated. The delegate may opt *not* to return a
// help bubble in certain circumstances, e.g. if there is an ongoing tutorial.
EXPECT_CALL(*user_education_delegate(),
CreateHelpBubble(Eq(primary_user_account_id),
CreateHelpBubble(Eq(primary_user_account_id()),
Eq(HelpBubbleId::kTest), A<HelpBubbleParams>(),
Eq(kElementId), Eq(element_context)))
.WillOnce(Return(ByMove(nullptr)));
Expand All @@ -132,23 +196,12 @@ TEST_F(UserEducationHelpBubbleControllerTest, CreateHelpBubble) {
// so, the delegate will return a `help_bubble` for the `controller()` to own.
HelpBubble* help_bubble = nullptr;
EXPECT_CALL(*user_education_delegate(),
CreateHelpBubble(Eq(primary_user_account_id),
CreateHelpBubble(Eq(primary_user_account_id()),
Eq(HelpBubbleId::kTest), A<HelpBubbleParams>(),
Eq(kElementId), Eq(element_context)))
.WillOnce(WithArgs<2>(Invoke([&](HelpBubbleParams help_bubble_params) {
// Set `help_bubble_id` in extended properties.
help_bubble_params.extended_properties.values().Merge(std::move(
user_education_util::CreateExtendedProperties(HelpBubbleId::kTest)
.values()));

// Create and cache the `help_bubble`.
auto result = help_bubble_factory()->CreateBubble(
ui::ElementTracker::GetElementTracker()->GetFirstMatchingElement(
kElementId, element_context),
std::move(help_bubble_params));
help_bubble = result.get();
return result;
})));
.WillOnce(WithArgs<2>(InvokeAndCopyResultAddressTo(
this, &UserEducationHelpBubbleControllerTest::CreateHelpBubble,
&help_bubble)));

// When the delegate returns a `help_bubble`, the `controller()` should
// indicate to the caller that a `help_bubble` was created.
Expand Down Expand Up @@ -195,23 +248,12 @@ TEST_F(UserEducationHelpBubbleControllerTest, CreateHelpBubble) {
// Once the `help_bubble` has been closed, the delegate should again be tasked
// with subsequent `help_bubble` creation.
EXPECT_CALL(*user_education_delegate(),
CreateHelpBubble(Eq(primary_user_account_id),
CreateHelpBubble(Eq(primary_user_account_id()),
Eq(HelpBubbleId::kTest), A<HelpBubbleParams>(),
Eq(kElementId), Eq(element_context)))
.WillOnce(WithArgs<2>(Invoke([&](HelpBubbleParams help_bubble_params) {
// Set `help_bubble_id` in extended properties.
help_bubble_params.extended_properties.values().Merge(std::move(
user_education_util::CreateExtendedProperties(HelpBubbleId::kTest)
.values()));

// Create and cache the `help_bubble`.
auto result = help_bubble_factory()->CreateBubble(
ui::ElementTracker::GetElementTracker()->GetFirstMatchingElement(
kElementId, element_context),
std::move(help_bubble_params));
help_bubble = result.get();
return result;
})));
.WillOnce(WithArgs<2>(InvokeAndCopyResultAddressTo(
this, &UserEducationHelpBubbleControllerTest::CreateHelpBubble,
&help_bubble)));

// The `controller()` should indicate to the caller success when attempting to
// create a new `help_bubble` since the previous `help_bubble` was closed.
Expand Down Expand Up @@ -244,4 +286,60 @@ TEST_F(UserEducationHelpBubbleControllerTest, CreateHelpBubble) {
EXPECT_FALSE(controller()->GetHelpBubbleId(kElementId, ui::ElementContext()));
}

// Verifies that `UserEducationHelpBubbleController` subscriptions are WAI.
TEST_F(UserEducationHelpBubbleControllerTest, Subscriptions) {
// When the `user_education_delegate()` is asked to create a help bubble, do
// so and cache a pointer to the result.
HelpBubble* help_bubble = nullptr;
ON_CALL(*user_education_delegate(), CreateHelpBubble)
.WillByDefault(WithArgs<2>(InvokeAndCopyResultAddressTo(
this, &UserEducationHelpBubbleControllerTest::CreateHelpBubble,
&help_bubble)));

{
// Expect that subscribers will be notified of shown events.
// Note that help bubbles are shown automatically when created.
base::test::RepeatingTestFuture<void> event_future;
base::CallbackListSubscription subscription =
controller()->AddHelpBubbleShownCallback(event_future.GetCallback());

// Create the `help_bubble`.
EXPECT_TRUE(controller()->CreateHelpBubble(HelpBubbleId::kTest,
HelpBubbleParams(), kElementId,
help_bubble_anchor_context()));

// Verify expectations.
EXPECT_TRUE(event_future.Wait());
}

{
// Expect that subscribers will be notified of anchor bounds changed events.
base::test::RepeatingTestFuture<void> event_future;
base::CallbackListSubscription subscription =
controller()->AddHelpBubbleAnchorBoundsChangedCallback(
event_future.GetCallback());

// Change `help_bubble` anchor bounds.
help_bubble_anchor_widget()->CenterWindow(gfx::Size(100, 100));

// Verify expectations.
EXPECT_TRUE(event_future.Wait());
}

{
// Expect that subscribers will be notified of closed events.
base::test::RepeatingTestFuture<void> event_future;
base::CallbackListSubscription subscription =
controller()->AddHelpBubbleClosedCallback(event_future.GetCallback());

// Close the `help_bubble`.
ASSERT_TRUE(help_bubble);
help_bubble->Close();
help_bubble = nullptr;

// Verify expectations.
EXPECT_TRUE(event_future.Wait());
}
}

} // namespace ash

0 comments on commit 7f72814

Please sign in to comment.