blob: ab8cc7d34564c3cb881ab71a11ce036409bafddf [file] [log] [blame]
// 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/user_education_ping_controller.h"
#include <memory>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/user_education/user_education_ash_test_base.h"
#include "ash/user_education/user_education_class_properties.h"
#include "ash/user_education/user_education_types.h"
#include "base/barrier_closure.h"
#include "base/functional/callback_helpers.h"
#include "base/functional/function_ref.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/layer_observer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// Aliases.
using ::base::test::RunClosure;
using ::testing::AllOf;
using ::testing::Conditional;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Optional;
using ::testing::Property;
// Helpers ---------------------------------------------------------------------
gfx::Rect Inset(const gfx::Rect& rect, const gfx::Insets& insets) {
gfx::Rect inset_rect(rect);
inset_rect.Inset(insets);
return inset_rect;
}
gfx::Rect Inset(const gfx::Rect& rect, const gfx::Insets* insets) {
return insets ? Inset(rect, *insets) : rect;
}
// MockLayerObserver -----------------------------------------------------------
class MockLayerObserver : public ui::LayerObserver {
public:
// ui::LayerObserver:
MOCK_METHOD(void, LayerDestroyed, (ui::Layer * layer), (override));
};
} // namespace
// UserEducationPingControllerTest ---------------------------------------------
// Base class for tests of the `UserEducationPingController`.
class UserEducationPingControllerTest : public UserEducationAshTestBase {
public:
UserEducationPingControllerTest() {
// NOTE: The `UserEducationPingController` exists only when a user education
// feature is enabled. Controller existence is verified in test coverage for
// the controller's owner.
std::vector<base::test::FeatureRef> enabled_features;
enabled_features.emplace_back(features::kHoldingSpaceWallpaperNudge);
enabled_features.emplace_back(features::kWelcomeTour);
scoped_feature_list_.InitWithFeatures(enabled_features, {});
}
// Returns the singleton instance owned by the `UserEducationController`.
UserEducationPingController* controller() {
return UserEducationPingController::Get();
}
// Returns the view to ping.
views::View* view() { return widget_->GetContentsView(); }
// Creates a ping for the specified view, returning `true` if successful.
bool CreatePing(PingId ping_id = PingId::kTest1,
const std::optional<views::View*>& v = std::nullopt) {
return controller()->CreatePing(ping_id, v.value_or(this->view()));
}
// Expects that no ping exists for the specified view. No ping exists for a
// view if the view does not have any associated ping layers.
void ExpectNoPing(const std::optional<views::View*>& v = std::nullopt) {
views::View* const view = v.value_or(this->view());
EXPECT_THAT(
view->GetLayersInOrder(),
Conditional(view->layer(), ElementsAre(Eq(view->layer())), IsEmpty()));
}
// Asserts that a ping exists for the specified view. A ping exists for a view
// if the view has ping layers configured with expected properties.
void AssertPingProperties(
const std::optional<views::View*>& v = std::nullopt) {
views::View* const view = v.value_or(this->view());
using testing::Pointee;
ASSERT_THAT(
view->GetLayersInOrder(),
ElementsAre(
AllOf(
Property(&ui::Layer::name,
Eq(UserEducationPingController::kPingParentLayerName)),
Property(&ui::Layer::bounds, Eq(view->layer()->bounds())),
Property(&ui::Layer::type, Eq(ui::LAYER_NOT_DRAWN)),
Property(
&ui::Layer::children,
ElementsAre(AllOf(
Pointee(Property(&ui::Layer::name,
Eq(UserEducationPingController::
kPingChildLayerName))),
Pointee(Property(&ui::Layer::background_color,
Eq(DarkLightModeController::Get()
->IsDarkModeEnabled()
? SK_ColorWHITE
: SK_ColorBLACK))),
Pointee(Property(
&ui::Layer::bounds,
Eq(Inset(gfx::Rect(view->layer()->bounds().size()),
view->GetProperty(kPingInsetsKey))))),
Pointee(Property(
&ui::Layer::rounded_corner_radii,
Eq(gfx::RoundedCornersF(
Inset(gfx::Rect(view->layer()->bounds().size()),
view->GetProperty(kPingInsetsKey))
.width() /
2.f)))),
Pointee(Property(&ui::Layer::type,
Eq(ui::LAYER_SOLID_COLOR))))))),
Eq(view->layer())));
}
// Asserts that a ping exists for `view()` and that the ping is destroyed by
// invoking the specified `destroy_function`. A ping is considered destroyed
// for `view()` when its associated ping layers are destroyed. The optionally
// specified `ping_layers_destroyed_callback` will be run immediately after
// destruction of all ping layers.
void AssertPingDestruction(
base::FunctionRef<void()> destroy_function,
base::OnceClosure ping_layers_destroyed_callback = base::DoNothing()) {
// Assert that a ping exists for `view()`.
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
// Observe ping layers.
NiceMock<MockLayerObserver> layer_observer;
view()->GetLayersInOrder()[0]->AddObserver(&layer_observer);
view()->GetLayersInOrder()[0]->children()[0]->AddObserver(&layer_observer);
// Create a barrier closure to invoke `ping_layers_destroyed_callback` once
// all ping layers have been destroyed.
base::RepeatingCallback ping_layer_destroyed_callback =
base::BarrierClosure(2u, std::move(ping_layers_destroyed_callback));
// Expect parent ping layer destruction and invoke
// `ping_layer_destroyed_callback` when that occurs.
EXPECT_CALL(layer_observer,
LayerDestroyed(Property(
&ui::Layer::name,
Eq(UserEducationPingController::kPingParentLayerName))))
.WillOnce(RunClosure(ping_layer_destroyed_callback));
// Expect child ping layer destruction and invoke
// `ping_layer_destroyed_callback` when that occurs.
EXPECT_CALL(layer_observer,
LayerDestroyed(Property(
&ui::Layer::name,
Eq(UserEducationPingController::kPingChildLayerName))))
.WillOnce(RunClosure(ping_layer_destroyed_callback));
// Invoked `destroy_function` and expect the ping has been destroyed.
destroy_function();
Mock::VerifyAndClearExpectations(&layer_observer);
ExpectNoPing();
}
private:
// UserEducationPingControllerTest:
void SetUp() override {
UserEducationAshTestBase::SetUp();
// Simulate user login so that a pref service is activated as is needed by
// the dark/light mode controller.
SimulateUserLogin("user@test");
// Create a `widget_` whose contents `view()` will be pinged.
widget_ = CreateFramelessTestWidget();
widget_->SetContentsView(
views::Builder<views::View>().SetPaintToLayer().Build());
widget_->CenterWindow(gfx::Size(100, 100));
// Use a non-zero animation duration so that an interval exists between
// ping creation and when we are able to check for ping existence since
// pings are automatically destroyed on animation ended/aborted.
scoped_animation_duration_scale_mode_ =
std::make_unique<ui::ScopedAnimationDurationScaleMode>(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
}
// Used to enable user education features which are required for existence of
// the `controller()` under test.
base::test::ScopedFeatureList scoped_feature_list_;
// Owns the `view()` to ping.
std::unique_ptr<views::Widget> widget_;
// Used to force a non-zero animation duration.
std::unique_ptr<ui::ScopedAnimationDurationScaleMode>
scoped_animation_duration_scale_mode_;
};
// Tests -----------------------------------------------------------------------
// Verifies that pings are destroyed if animations are aborted.
TEST_F(UserEducationPingControllerTest, Abort) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
ASSERT_NO_FATAL_FAILURE(AssertPingDestruction([&]() {
view()
->GetLayersInOrder()[0]
->children()[0]
->GetAnimator()
->StopAnimating();
}));
}
// Verifies that pings are destroyed if animations are ended.
TEST_F(UserEducationPingControllerTest, End) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
// Note that `ping_animation_ended_future` will wait until all ping layers
// have been destroyed due to the ping animation having ended.
base::test::TestFuture<void> ping_animation_ended_future;
ASSERT_NO_FATAL_FAILURE(AssertPingDestruction(
[&]() { EXPECT_TRUE(ping_animation_ended_future.Wait()); },
ping_animation_ended_future.GetCallback()));
}
// Verifies that `UserEducationPingController::GetPingId()` is WAI.
TEST_F(UserEducationPingControllerTest, GetPingId) {
views::View other_view;
ExpectNoPing();
EXPECT_FALSE(controller()->GetPingId(view()));
EXPECT_FALSE(controller()->GetPingId(&other_view));
EXPECT_TRUE(CreatePing());
EXPECT_THAT(controller()->GetPingId(view()), Optional(PingId::kTest1));
EXPECT_FALSE(controller()->GetPingId(&other_view));
view()->SetVisible(false);
EXPECT_FALSE(controller()->GetPingId(view()));
EXPECT_FALSE(controller()->GetPingId(&other_view));
}
// Verifies that a single view cannot be pinged multiple times concurrently but
// that multiple concurrent pings may exist for distinct views.
TEST_F(UserEducationPingControllerTest, MultipleConcurrent) {
ExpectNoPing();
// Ping `view()`.
EXPECT_TRUE(CreatePing(PingId::kTest1));
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
// Attempts to ping `view()` multiple times concurrently should fail, even if
// no ping yet exists for the specified ping ID.
EXPECT_FALSE(CreatePing(PingId::kTest1));
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
EXPECT_FALSE(CreatePing(PingId::kTest2));
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
// Add `another_view` to the view hierarchy.
views::View* another_view = nullptr;
view()->AddChildView(views::Builder<views::View>()
.CopyAddressTo(&another_view)
.SetPaintToLayer()
.Build());
// Attempts to ping `another_view` should succeed if and only if no ping yet
// exists for the specified ping ID.
EXPECT_FALSE(CreatePing(PingId::kTest1, another_view));
ExpectNoPing(another_view);
EXPECT_TRUE(CreatePing(PingId::kTest2, another_view));
ASSERT_NO_FATAL_FAILURE(AssertPingProperties(another_view));
}
// Verifies that the same `view()` can be pinged multiple times sequentially.
TEST_F(UserEducationPingControllerTest, MultipleSequential) {
ExpectNoPing();
{
// Use a zero duration animation scale mode so that the first ping animation
// will end instantaneously without requiring for us to wait.
ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
EXPECT_TRUE(CreatePing());
ExpectNoPing();
}
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
}
// Verifies that a `view()` cannot be pinged if not drawn.
TEST_F(UserEducationPingControllerTest, NotIsDrawn) {
view()->SetVisible(false);
ExpectNoPing();
EXPECT_FALSE(CreatePing());
ExpectNoPing();
}
// Verifies that pings will update bounds when bounds are updated for the
// associated `view()`.
TEST_F(UserEducationPingControllerTest, OnViewBoundsChanged) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
view()->SetBoundsRect(Inset(view()->bounds(), gfx::Insets(10)));
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
}
// Verifies that pings are destroyed when the associated `view()` is deleting.
TEST_F(UserEducationPingControllerTest, OnViewIsDeleting) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
// Note that setting a new contents view will result in the destruction of
// the original contents `view()` associated with the ping.
ASSERT_NO_FATAL_FAILURE(AssertPingDestruction([&]() {
view()->GetWidget()->SetContentsView(views::Builder<views::View>().Build());
}));
}
// Verifies that pings will update bounds when ping insets are updated for the
// associated `view()`.
TEST_F(UserEducationPingControllerTest, OnViewPropertyChanged) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
view()->SetProperty(kPingInsetsKey, gfx::Insets(10));
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
}
// Verifies that pings will update colors when theme is updated for the
// associated `view()`.
TEST_F(UserEducationPingControllerTest, OnViewThemeChanged) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
auto* const controller = DarkLightModeController::Get();
controller->SetDarkModeEnabledForTest(!controller->IsDarkModeEnabled());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
}
// Verifies that pings are destroyed if `view()` stops being visible.
TEST_F(UserEducationPingControllerTest, OnViewVisibilityChanged) {
ExpectNoPing();
EXPECT_TRUE(CreatePing());
ASSERT_NO_FATAL_FAILURE(AssertPingProperties());
ASSERT_NO_FATAL_FAILURE(
AssertPingDestruction([&]() { view()->SetVisible(false); }));
}
} // namespace ash