| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/assistant/assistant_notification_controller_impl.h" |
| |
| #include <map> |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/assistant/assistant_controller_impl.h" |
| #include "ash/assistant/model/assistant_notification_model_observer.h" |
| #include "ash/assistant/test/test_assistant_service.h" |
| #include "ash/shell.h" |
| #include "ash/test/ash_test_base.h" |
| #include "base/test/task_environment.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/services/assistant/public/cpp/assistant_service.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/message_center/message_center.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using chromeos::assistant::AssistantNotification; |
| using chromeos::assistant::AssistantNotificationButton; |
| using chromeos::assistant::AssistantNotificationPriority; |
| |
| using testing::_; |
| using testing::Eq; |
| using testing::Field; |
| using testing::Mock; |
| using testing::StrictMock; |
| |
| // Constants. |
| constexpr bool kAnyBool = false; |
| |
| // Matchers -------------------------------------------------------------------- |
| |
| MATCHER_P(IdIs, expected_id, "") { |
| if (arg.client_id != expected_id) { |
| *result_listener << "Received notification with a wrong id.\n" |
| << "Expected:\n '" << expected_id << "'\n" |
| << "Actual:\n '" << arg.client_id << "'\n"; |
| return false; |
| } |
| return true; |
| } |
| |
| // Builders -------------------------------------------------------------------- |
| |
| class AssistantNotificationBuilder { |
| public: |
| AssistantNotificationBuilder() = default; |
| |
| AssistantNotificationBuilder(const AssistantNotificationBuilder& bldr) { |
| notification_ = bldr.notification_; |
| } |
| |
| ~AssistantNotificationBuilder() = default; |
| |
| AssistantNotification Build() const { return notification_; } |
| |
| AssistantNotificationBuilder& WithId(const std::string& id) { |
| notification_.client_id = id; |
| notification_.server_id = id; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithActionUrl(const GURL& action_url) { |
| notification_.action_url = action_url; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithButton(AssistantNotificationButton&& button, |
| int index = -1) { |
| if (index != -1) { |
| notification_.buttons.insert(notification_.buttons.begin() + index, |
| std::move(button)); |
| } else { |
| notification_.buttons.push_back(std::move(button)); |
| } |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithFromServer(bool from_server) { |
| notification_.from_server = from_server; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithIsPinned(bool is_pinned) { |
| notification_.is_pinned = is_pinned; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithPriority( |
| AssistantNotificationPriority priority) { |
| notification_.priority = priority; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithRemoveOnClick(bool remove_on_click) { |
| notification_.remove_on_click = remove_on_click; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithRenotify(bool renotify) { |
| notification_.renotify = renotify; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithTimeout( |
| base::Optional<base::TimeDelta> timeout) { |
| notification_.expiry_time = |
| timeout.has_value() |
| ? base::Optional<base::Time>(base::Time::Now() + timeout.value()) |
| : base::nullopt; |
| return *this; |
| } |
| |
| AssistantNotificationBuilder& WithTimeoutMs(int timeout_ms) { |
| return WithTimeout(base::TimeDelta::FromMilliseconds(timeout_ms)); |
| } |
| |
| private: |
| AssistantNotification notification_; |
| }; |
| |
| class AssistantNotificationButtonBuilder { |
| public: |
| AssistantNotificationButtonBuilder() = default; |
| |
| AssistantNotificationButtonBuilder( |
| const AssistantNotificationButtonBuilder& bldr) { |
| button_ = bldr.button_; |
| } |
| |
| ~AssistantNotificationButtonBuilder() = default; |
| |
| AssistantNotificationButton Build() const { return button_; } |
| |
| AssistantNotificationButtonBuilder& WithLabel(const std::string& label) { |
| button_.label = label; |
| return *this; |
| } |
| |
| AssistantNotificationButtonBuilder& WithActionUrl(const GURL& action_url) { |
| button_.action_url = action_url; |
| return *this; |
| } |
| |
| AssistantNotificationButtonBuilder& WithRemoveNotificationOnClick( |
| bool remove_notification_on_click) { |
| button_.remove_notification_on_click = remove_notification_on_click; |
| return *this; |
| } |
| |
| private: |
| AssistantNotificationButton button_; |
| }; |
| |
| // Mocks ----------------------------------------------------------------------- |
| |
| class AssistantNotificationModelObserverMock |
| : public AssistantNotificationModelObserver { |
| public: |
| AssistantNotificationModelObserverMock() = default; |
| ~AssistantNotificationModelObserverMock() override = default; |
| |
| MOCK_METHOD(void, |
| OnNotificationAdded, |
| (const AssistantNotification& notification), |
| (override)); |
| MOCK_METHOD(void, |
| OnNotificationUpdated, |
| (const AssistantNotification& notification), |
| (override)); |
| MOCK_METHOD(void, |
| OnNotificationRemoved, |
| (const AssistantNotification& notification, bool from_server), |
| (override)); |
| MOCK_METHOD(void, OnAllNotificationsRemoved, (bool from_server), (override)); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(AssistantNotificationModelObserverMock); |
| }; |
| |
| class AssistantServiceMock : public TestAssistantService { |
| public: |
| MOCK_METHOD(void, |
| RetrieveNotification, |
| (const AssistantNotification& notification, int action_index), |
| (override)); |
| }; |
| |
| // AssistantNotificationControllerTest ----------------------------------------- |
| |
| class AssistantNotificationControllerTest : public AshTestBase { |
| protected: |
| AssistantNotificationControllerTest() |
| : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| ~AssistantNotificationControllerTest() override = default; |
| |
| void SetUp() override { |
| AshTestBase::SetUp(); |
| |
| controller_ = |
| Shell::Get()->assistant_controller()->notification_controller(); |
| DCHECK(controller_); |
| } |
| |
| AssistantNotificationControllerImpl& controller() { return *controller_; } |
| |
| AssistantNotificationModelObserverMock& AddStrictObserverMock() { |
| observer_ = |
| std::make_unique<StrictMock<AssistantNotificationModelObserverMock>>(); |
| controller().model()->AddObserver(observer_.get()); |
| return *observer_; |
| } |
| |
| void AddOrUpdateNotification(AssistantNotification&& notification) { |
| controller().AddOrUpdateNotification(std::move(notification)); |
| } |
| |
| void RemoveNotification(const std::string& id) { |
| controller().RemoveNotificationById(id, kAnyBool); |
| } |
| |
| void ForwardTimeInMs(int time_in_ms) { |
| task_environment()->FastForwardBy( |
| base::TimeDelta::FromMilliseconds(time_in_ms)); |
| } |
| |
| private: |
| AssistantNotificationControllerImpl* controller_; |
| std::unique_ptr<AssistantNotificationModelObserverMock> observer_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AssistantNotificationControllerTest); |
| }; |
| |
| } // namespace |
| |
| // Tests ----------------------------------------------------------------------- |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldInformObserverOfNewNotifications) { |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationAdded(IdIs("id"))); |
| controller().AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId("id").Build()); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldInformObserverOfUpdatedNotifications) { |
| const auto builder = AssistantNotificationBuilder().WithId("id"); |
| controller().AddOrUpdateNotification(builder.Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationUpdated(IdIs("id"))); |
| controller().AddOrUpdateNotification(builder.Build()); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldInformObserverOfRemovedNotifications) { |
| constexpr bool kFromServer = kAnyBool; |
| |
| auto builder = AssistantNotificationBuilder().WithId("id"); |
| controller().AddOrUpdateNotification(builder.Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("id"), Eq(kFromServer))); |
| controller().RemoveNotificationById(builder.Build().client_id, kFromServer); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldInformObserverOfRemoveAllNotifications) { |
| constexpr bool kFromServer = !kAnyBool; |
| |
| controller().AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId("id").Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnAllNotificationsRemoved(Eq(kFromServer))); |
| controller().RemoveAllNotifications(kFromServer); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldRemoveNotificationWhenItExpires) { |
| constexpr int kTimeoutMs = 1000; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("id") |
| .WithTimeoutMs((kTimeoutMs)) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("id"), _)); |
| ForwardTimeInMs(kTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldNotRemoveNotificationsTooSoon) { |
| constexpr int kTimeoutMs = 1000; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("id") |
| .WithTimeoutMs(kTimeoutMs) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved).Times(0); |
| ForwardTimeInMs(kTimeoutMs - 1); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldUseFromServerFalseWhenNotificationExpires) { |
| constexpr int kTimeoutMs = 1000; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("id") |
| .WithTimeoutMs(kTimeoutMs) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(_, Eq(false))); |
| ForwardTimeInMs(kTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldRemoveEachNotificationAsItExpires) { |
| constexpr int kFirstTimeoutMs = 1000; |
| constexpr int kSecondTimeoutMs = 1500; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("first") |
| .WithTimeoutMs(kFirstTimeoutMs) |
| .Build()); |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("second") |
| .WithTimeoutMs(kSecondTimeoutMs) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("first"), _)); |
| ForwardTimeInMs(kFirstTimeoutMs); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("second"), _)); |
| ForwardTimeInMs(kSecondTimeoutMs - kFirstTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldSupport2NotificationsThatExpireAtTheSameTime) { |
| constexpr int kTimeoutMs = 1000; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("first") |
| .WithTimeoutMs(kTimeoutMs) |
| .Build()); |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("at-same-time") |
| .WithTimeoutMs(kTimeoutMs) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("first"), _)); |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("at-same-time"), _)); |
| ForwardTimeInMs(kTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldImmediateRemoveNotificationsThatAlreadyExpired) { |
| constexpr int kNegativeTimeoutMs = -1000; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("expired") |
| .WithTimeoutMs(kNegativeTimeoutMs) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs("expired"), _)); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldNotRemoveNotificationsThatWereManuallyRemoved) { |
| constexpr int kTimeoutMs = 1000; |
| |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId("id") |
| .WithTimeoutMs(kTimeoutMs) |
| .Build()); |
| RemoveNotification("id"); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved).Times(0); |
| ForwardTimeInMs(kTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldSupportExpiryTimeSetInUpdate) { |
| constexpr int kTimeoutMs = 1000; |
| |
| auto notification_bldr = AssistantNotificationBuilder().WithId("id"); |
| |
| AddOrUpdateNotification(notification_bldr.Build()); |
| AddOrUpdateNotification(notification_bldr.WithTimeoutMs(kTimeoutMs).Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved); |
| ForwardTimeInMs(kTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldNotRemoveNotificationIfExpiryTimeIsClearedInUpdate) { |
| constexpr int kTimeoutMs = 1000; |
| |
| auto notification_bldr = AssistantNotificationBuilder().WithId("id"); |
| |
| AddOrUpdateNotification(notification_bldr.WithTimeoutMs(kTimeoutMs).Build()); |
| AddOrUpdateNotification(notification_bldr.WithTimeout(base::nullopt).Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved).Times(0); |
| ForwardTimeInMs(kTimeoutMs); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldMaybeRemoveNotificationWhenClicking) { |
| constexpr char kId[] = "id"; |
| |
| auto notification_bldr = |
| AssistantNotificationBuilder().WithId(kId).WithActionUrl( |
| GURL("https://g.co/")); |
| |
| AddOrUpdateNotification(notification_bldr.WithRemoveOnClick(false).Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| auto* message_center = message_center::MessageCenter::Get(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved).Times(0); |
| message_center->ClickOnNotification(kId); |
| |
| Mock::VerifyAndClearExpectations(&observer); |
| |
| EXPECT_CALL(observer, OnNotificationUpdated(IdIs(kId))); |
| AddOrUpdateNotification(notification_bldr.WithRemoveOnClick(true).Build()); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs(kId), _)); |
| message_center->ClickOnNotification(kId); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldMaybeRemoveNotificationWhenClickingButton) { |
| constexpr char kId[] = "id"; |
| |
| auto notification_bldr = AssistantNotificationBuilder().WithId(kId); |
| auto button_bldr = |
| AssistantNotificationButtonBuilder().WithActionUrl(GURL("https://g.co/")); |
| |
| AddOrUpdateNotification( |
| notification_bldr |
| .WithButton(button_bldr.WithRemoveNotificationOnClick(false).Build()) |
| .Build()); |
| |
| auto& observer = AddStrictObserverMock(); |
| |
| auto* message_center = message_center::MessageCenter::Get(); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved).Times(0); |
| message_center->ClickOnNotificationButton(kId, /*index=*/0); |
| |
| Mock::VerifyAndClearExpectations(&observer); |
| |
| EXPECT_CALL(observer, OnNotificationUpdated(IdIs(kId))); |
| AddOrUpdateNotification( |
| notification_bldr |
| .WithButton(button_bldr.WithRemoveNotificationOnClick(true).Build(), |
| /*index=*/0) |
| .Build()); |
| |
| EXPECT_CALL(observer, OnNotificationRemoved(IdIs(kId), _)); |
| message_center->ClickOnNotificationButton(kId, /*index=*/0); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldCorrectlyMapNotificationPriority) { |
| constexpr char kId[] = "id"; |
| |
| // Map of Assistant notification priorities to system notification priorities. |
| const std::map<AssistantNotificationPriority, |
| message_center::NotificationPriority> |
| priority_map = {{AssistantNotificationPriority::kLow, |
| message_center::NotificationPriority::LOW_PRIORITY}, |
| {AssistantNotificationPriority::kDefault, |
| message_center::NotificationPriority::DEFAULT_PRIORITY}, |
| {AssistantNotificationPriority::kHigh, |
| message_center::NotificationPriority::HIGH_PRIORITY}}; |
| |
| for (const auto& priority_pair : priority_map) { |
| // Create an Assistant notification. |
| AddOrUpdateNotification(AssistantNotificationBuilder() |
| .WithId(kId) |
| .WithPriority(priority_pair.first) |
| .Build()); |
| |
| // Verify expected system notification. |
| auto* system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_EQ(priority_pair.second, system_notification->priority()); |
| } |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, ShouldPropagateIsPinned) { |
| constexpr char kId[] = "id"; |
| |
| // Create an Assistant notification w/ default pin behavior. |
| AddOrUpdateNotification(AssistantNotificationBuilder().WithId(kId).Build()); |
| |
| // Verify expected system notification. |
| auto* system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_FALSE(system_notification->pinned()); |
| |
| // Create an Assistant notification w/ explicitly disabled pin behavior. |
| AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId(kId).WithIsPinned(false).Build()); |
| |
| // Verify expected system notification. |
| system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_FALSE(system_notification->pinned()); |
| |
| // Create an Assistant notification w/ explicitly enabled pin behavior. |
| AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId(kId).WithIsPinned(true).Build()); |
| |
| // Verify expected system notification. |
| system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_TRUE(system_notification->pinned()); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, ShouldPropagateRenotify) { |
| constexpr char kId[] = "id"; |
| |
| // Create an Assistant notification w/ default renotify behavior. |
| AddOrUpdateNotification(AssistantNotificationBuilder().WithId(kId).Build()); |
| |
| // Verify expected system notification. |
| auto* system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_FALSE(system_notification->renotify()); |
| |
| // Create an Assistant notification w/ explicitly disabled renotify behavior. |
| AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId(kId).WithRenotify(false).Build()); |
| |
| // Verify expected system notification. |
| system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_FALSE(system_notification->renotify()); |
| |
| // Create an Assistant notification w/ explicitly enabled renotify behavior. |
| AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId(kId).WithRenotify(true).Build()); |
| |
| // Verify expected system notification. |
| system_notification = |
| message_center::MessageCenter::Get()->FindVisibleNotificationById(kId); |
| ASSERT_NE(nullptr, system_notification); |
| EXPECT_TRUE(system_notification->renotify()); |
| } |
| |
| TEST_F(AssistantNotificationControllerTest, |
| ShouldMaybeRetrieveNotificationPayload) { |
| constexpr char kId[] = "id"; |
| |
| // Mock the Assistant service. |
| StrictMock<AssistantServiceMock> service; |
| controller().SetAssistant(&service); |
| |
| // Create an Assistant notification w/ default |from_server|. |
| AddOrUpdateNotification(AssistantNotificationBuilder().WithId(kId).Build()); |
| |
| // Click notification and verify we do *not* attempt to retrieve payload. |
| EXPECT_CALL(service, RetrieveNotification).Times(0); |
| message_center::MessageCenter::Get()->ClickOnNotification(kId); |
| Mock::VerifyAndClearExpectations(&service); |
| |
| // Create an Assistant notification explicitly *not* |from_server|. |
| AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId(kId).WithFromServer(false).Build()); |
| |
| // Click notification and verify we do *not* attempt to retrieve payload. |
| EXPECT_CALL(service, RetrieveNotification).Times(0); |
| message_center::MessageCenter::Get()->ClickOnNotification(kId); |
| Mock::VerifyAndClearExpectations(&service); |
| |
| // Create an Assistant notification explicitly |from_server|. |
| AddOrUpdateNotification( |
| AssistantNotificationBuilder().WithId(kId).WithFromServer(true).Build()); |
| |
| // Click notification and verify we *do* attempt to retrieve payload. |
| EXPECT_CALL(service, RetrieveNotification); |
| message_center::MessageCenter::Get()->ClickOnNotification(kId); |
| } |
| |
| } // namespace ash |