blob: ae199f1ae22d7ddbc3e0f239b95a686554088662 [file] [log] [blame]
// Copyright 2014 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 <stdint.h>
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/cxx17_backports.h"
#include "base/feature_list.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/notifications/metrics/mock_notification_metrics_logger.h"
#include "chrome/browser/notifications/metrics/notification_metrics_logger_factory.h"
#include "chrome/browser/notifications/notification_display_service_impl.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/notifications/platform_notification_service_factory.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/testing_profile.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/test/test_history_database.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/buildflags/buildflags.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/notifications/notification_resources.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"
#include "third_party/blink/public/mojom/notifications/notification.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/value_builder.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
#if !defined(OS_ANDROID)
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/web_app_icon_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_icon_generator.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#endif
using blink::NotificationResources;
using blink::PlatformNotificationData;
using content::NotificationDatabaseData;
using message_center::Notification;
namespace {
const int kNotificationVibrationPattern[] = { 100, 200, 300 };
const char kNotificationId[] = "my-notification-id";
const char kClosedReason[] = "ClosedReason";
const char kDidReplaceAnotherNotification[] = "DidReplaceAnotherNotification";
const char kHasBadge[] = "HasBadge";
const char kHasImage[] = "HasImage";
const char kHasIcon[] = "HasIcon";
const char kHasRenotify[] = "HasRenotify";
const char kHasTag[] = "HasTag";
const char kIsSilent[] = "IsSilent";
const char kNumActions[] = "NumActions";
const char kNumClicks[] = "NumClicks";
const char kNumActionButtonClicks[] = "NumActionButtonClicks";
const char kRequireInteraction[] = "RequireInteraction";
const char kTimeUntilCloseMillis[] = "TimeUntilClose";
const char kTimeUntilFirstClickMillis[] = "TimeUntilFirstClick";
const char kTimeUntilLastClickMillis[] = "TimeUntilLastClick";
} // namespace
class PlatformNotificationServiceTest : public testing::Test {
public:
void SetUp() override {
display_service_tester_ =
std::make_unique<NotificationDisplayServiceTester>(&profile_);
mock_logger_ = static_cast<MockNotificationMetricsLogger*>(
NotificationMetricsLoggerFactory::GetInstance()
->SetTestingFactoryAndUse(
&profile_,
base::BindRepeating(
&MockNotificationMetricsLogger::FactoryForTests)));
recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
void TearDown() override {
display_service_tester_.reset();
}
protected:
// Returns the Platform Notification Service these unit tests are for.
PlatformNotificationServiceImpl* service() {
return PlatformNotificationServiceFactory::GetForProfile(&profile_);
}
size_t GetNotificationCountForType(NotificationHandler::Type type) {
return display_service_tester_->GetDisplayedNotificationsForType(type)
.size();
}
Notification GetDisplayedNotificationForType(NotificationHandler::Type type) {
std::vector<Notification> notifications =
display_service_tester_->GetDisplayedNotificationsForType(type);
DCHECK_EQ(1u, notifications.size());
return notifications[0];
}
protected:
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
std::unique_ptr<NotificationDisplayServiceTester> display_service_tester_;
// Owned by the |profile_| as a keyed service.
MockNotificationMetricsLogger* mock_logger_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> recorder_;
};
TEST_F(PlatformNotificationServiceTest, DisplayNonPersistentThenClose) {
ASSERT_TRUE(profile_.CreateHistoryService());
PlatformNotificationData data;
data.title = u"My Notification";
data.body = u"Hello, world!";
service()->DisplayNotification(kNotificationId, GURL("https://chrome.com/"),
/*document_url=*/GURL(), data,
NotificationResources());
EXPECT_EQ(1u, GetNotificationCountForType(
NotificationHandler::Type::WEB_NON_PERSISTENT));
service()->CloseNotification(kNotificationId);
EXPECT_EQ(0u, GetNotificationCountForType(
NotificationHandler::Type::WEB_NON_PERSISTENT));
}
TEST_F(PlatformNotificationServiceTest, DisplayPersistentThenClose) {
ASSERT_TRUE(profile_.CreateHistoryService());
PlatformNotificationData data;
data.title = u"My notification's title";
data.body = u"Hello, world!";
EXPECT_CALL(*mock_logger_, LogPersistentNotificationShown());
service()->DisplayPersistentNotification(
kNotificationId, GURL() /* service_worker_scope */,
GURL("https://chrome.com/"), data, NotificationResources());
ASSERT_EQ(1u, GetNotificationCountForType(
NotificationHandler::Type::WEB_PERSISTENT));
Notification notification = GetDisplayedNotificationForType(
NotificationHandler::Type::WEB_PERSISTENT);
EXPECT_EQ("https://chrome.com/", notification.origin_url().spec());
EXPECT_EQ("My notification's title",
base::UTF16ToUTF8(notification.title()));
EXPECT_EQ("Hello, world!",
base::UTF16ToUTF8(notification.message()));
service()->ClosePersistentNotification(kNotificationId);
EXPECT_EQ(0u, GetNotificationCountForType(
NotificationHandler::Type::WEB_PERSISTENT));
}
TEST_F(PlatformNotificationServiceTest, DisplayNonPersistentPropertiesMatch) {
ASSERT_TRUE(profile_.CreateHistoryService());
std::vector<int> vibration_pattern(
kNotificationVibrationPattern,
kNotificationVibrationPattern +
base::size(kNotificationVibrationPattern));
PlatformNotificationData data;
data.title = u"My notification's title";
data.body = u"Hello, world!";
data.vibration_pattern = vibration_pattern;
data.silent = true;
service()->DisplayNotification(kNotificationId, GURL("https://chrome.com/"),
/*document_url=*/GURL(), data,
NotificationResources());
ASSERT_EQ(1u, GetNotificationCountForType(
NotificationHandler::Type::WEB_NON_PERSISTENT));
Notification notification = GetDisplayedNotificationForType(
NotificationHandler::Type::WEB_NON_PERSISTENT);
EXPECT_EQ("https://chrome.com/", notification.origin_url().spec());
EXPECT_EQ("My notification's title",
base::UTF16ToUTF8(notification.title()));
EXPECT_EQ("Hello, world!",
base::UTF16ToUTF8(notification.message()));
EXPECT_THAT(notification.vibration_pattern(),
testing::ElementsAreArray(kNotificationVibrationPattern));
EXPECT_TRUE(notification.silent());
}
TEST_F(PlatformNotificationServiceTest, DisplayPersistentPropertiesMatch) {
ASSERT_TRUE(profile_.CreateHistoryService());
std::vector<int> vibration_pattern(
kNotificationVibrationPattern,
kNotificationVibrationPattern +
base::size(kNotificationVibrationPattern));
PlatformNotificationData data;
data.title = u"My notification's title";
data.body = u"Hello, world!";
data.vibration_pattern = vibration_pattern;
data.silent = true;
data.actions.resize(2);
data.actions[0] = blink::mojom::NotificationAction::New();
data.actions[0]->type = blink::mojom::NotificationActionType::BUTTON;
data.actions[0]->title = u"Button 1";
data.actions[1] = blink::mojom::NotificationAction::New();
data.actions[1]->type = blink::mojom::NotificationActionType::TEXT;
data.actions[1]->title = u"Button 2";
NotificationResources notification_resources;
notification_resources.action_icons.resize(data.actions.size());
EXPECT_CALL(*mock_logger_, LogPersistentNotificationShown());
service()->DisplayPersistentNotification(
kNotificationId, GURL() /* service_worker_scope */,
GURL("https://chrome.com/"), data, notification_resources);
ASSERT_EQ(1u, GetNotificationCountForType(
NotificationHandler::Type::WEB_PERSISTENT));
Notification notification = GetDisplayedNotificationForType(
NotificationHandler::Type::WEB_PERSISTENT);
EXPECT_EQ("https://chrome.com/", notification.origin_url().spec());
EXPECT_EQ("My notification's title", base::UTF16ToUTF8(notification.title()));
EXPECT_EQ("Hello, world!", base::UTF16ToUTF8(notification.message()));
EXPECT_THAT(notification.vibration_pattern(),
testing::ElementsAreArray(kNotificationVibrationPattern));
EXPECT_TRUE(notification.silent());
const auto& buttons = notification.buttons();
ASSERT_EQ(2u, buttons.size());
EXPECT_EQ("Button 1", base::UTF16ToUTF8(buttons[0].title));
EXPECT_FALSE(buttons[0].placeholder);
EXPECT_EQ("Button 2", base::UTF16ToUTF8(buttons[1].title));
EXPECT_TRUE(buttons[1].placeholder);
}
TEST_F(PlatformNotificationServiceTest, RecordNotificationUkmEvent) {
NotificationDatabaseData data;
data.notification_id = "notification1";
data.origin = GURL("https://example.com");
data.closed_reason = NotificationDatabaseData::ClosedReason::USER;
data.replaced_existing_notification = true;
data.notification_data.icon = GURL("https://icon.com");
data.notification_data.image = GURL("https://image.com");
data.notification_data.renotify = false;
data.notification_data.tag = "tag";
data.notification_data.silent = true;
auto action1 = blink::mojom::NotificationAction::New();
data.notification_data.actions.push_back(std::move(action1));
auto action2 = blink::mojom::NotificationAction::New();
data.notification_data.actions.push_back(std::move(action2));
auto action3 = blink::mojom::NotificationAction::New();
data.notification_data.actions.push_back(std::move(action3));
data.notification_data.require_interaction = false;
data.num_clicks = 3;
data.num_action_button_clicks = 1;
data.time_until_close_millis = base::Milliseconds(10000);
data.time_until_first_click_millis = base::Milliseconds(2222);
data.time_until_last_click_millis = base::Milliseconds(3333);
// Set up UKM recording conditions.
ASSERT_TRUE(profile_.CreateHistoryService());
auto* history_service = HistoryServiceFactory::GetForProfile(
&profile_, ServiceAccessType::EXPLICIT_ACCESS);
history_service->AddPage(data.origin, base::Time::Now(),
history::SOURCE_BROWSED);
// Initially there are no UKM entries.
std::vector<const ukm::mojom::UkmEntry*> entries =
recorder_->GetEntriesByName(ukm::builders::Notification::kEntryName);
EXPECT_EQ(0u, entries.size());
{
base::RunLoop run_loop;
service()->set_ukm_recorded_closure_for_testing(run_loop.QuitClosure());
service()->RecordNotificationUkmEvent(data);
run_loop.Run();
}
entries =
recorder_->GetEntriesByName(ukm::builders::Notification::kEntryName);
ASSERT_EQ(1u, entries.size());
auto* entry = entries[0];
recorder_->ExpectEntryMetric(
entry, kClosedReason,
static_cast<int>(NotificationDatabaseData::ClosedReason::USER));
recorder_->ExpectEntryMetric(entry, kDidReplaceAnotherNotification, true);
recorder_->ExpectEntryMetric(entry, kHasBadge, false);
recorder_->ExpectEntryMetric(entry, kHasIcon, 1);
recorder_->ExpectEntryMetric(entry, kHasImage, 1);
recorder_->ExpectEntryMetric(entry, kHasRenotify, false);
recorder_->ExpectEntryMetric(entry, kHasTag, true);
recorder_->ExpectEntryMetric(entry, kIsSilent, 1);
recorder_->ExpectEntryMetric(entry, kNumActions, 3);
recorder_->ExpectEntryMetric(entry, kNumActionButtonClicks, 1);
recorder_->ExpectEntryMetric(entry, kNumClicks, 3);
recorder_->ExpectEntryMetric(entry, kRequireInteraction, false);
recorder_->ExpectEntryMetric(entry, kTimeUntilCloseMillis, 10000);
recorder_->ExpectEntryMetric(entry, kTimeUntilFirstClickMillis, 2222);
recorder_->ExpectEntryMetric(entry, kTimeUntilLastClickMillis, 3333);
}
// Expect each call to ReadNextPersistentNotificationId to return a larger
// value.
TEST_F(PlatformNotificationServiceTest, NextPersistentNotificationId) {
int64_t first_id = service()->ReadNextPersistentNotificationId();
int64_t second_id = service()->ReadNextPersistentNotificationId();
EXPECT_LT(first_id, second_id);
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
TEST_F(PlatformNotificationServiceTest, DisplayNameForContextMessage) {
std::u16string display_name =
service()->DisplayNameForContextMessage(GURL("https://chrome.com/"));
EXPECT_TRUE(display_name.empty());
// Create a mocked extension.
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder()
.SetID("honijodknafkokifofgiaalefdiedpko")
.SetManifest(extensions::DictionaryBuilder()
.Set("name", "NotificationTest")
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("description", "Test Extension")
.Build())
.Build();
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(&profile_);
EXPECT_TRUE(registry->AddEnabled(extension));
display_name = service()->DisplayNameForContextMessage(
GURL("chrome-extension://honijodknafkokifofgiaalefdiedpko/main.html"));
EXPECT_EQ("NotificationTest", base::UTF16ToUTF8(display_name));
}
TEST_F(PlatformNotificationServiceTest, CreateNotificationFromData) {
PlatformNotificationData notification_data;
notification_data.title = u"My Notification";
notification_data.body = u"Hello, world!";
GURL origin("https://chrome.com/");
Notification notification = service()->CreateNotificationFromData(
origin, "id", notification_data, NotificationResources(),
/*web_app_hint_url=*/GURL());
EXPECT_TRUE(notification.context_message().empty());
// Create a mocked extension.
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder()
.SetID("honijodknafkokifofgiaalefdiedpko")
.SetManifest(extensions::DictionaryBuilder()
.Set("name", "NotificationTest")
.Set("version", "1.0")
.Set("manifest_version", 2)
.Set("description", "Test Extension")
.Build())
.Build();
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(&profile_);
EXPECT_TRUE(registry->AddEnabled(extension));
notification = service()->CreateNotificationFromData(
GURL("chrome-extension://honijodknafkokifofgiaalefdiedpko/main.html"),
"id", notification_data, NotificationResources(),
/*web_app_hint_url=*/GURL());
EXPECT_EQ("NotificationTest",
base::UTF16ToUTF8(notification.context_message()));
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
#if !defined(OS_ANDROID)
class PlatformNotificationServiceTest_WebAppNotificationIconAndTitle
: public PlatformNotificationServiceTest {
private:
base::test::ScopedFeatureList scoped_feature_list_{
features::kDesktopPWAsNotificationIconAndTitle};
};
TEST_F(PlatformNotificationServiceTest_WebAppNotificationIconAndTitle,
FindWebAppIconAndTitle_NoApp) {
web_app::FakeWebAppProvider* provider =
web_app::FakeWebAppProvider::Get(&profile_);
provider->Start();
const GURL web_app_url{"https://example.org/"};
EXPECT_FALSE(service()->FindWebAppIconAndTitle(web_app_url).has_value());
}
TEST_F(PlatformNotificationServiceTest_WebAppNotificationIconAndTitle,
FindWebAppIconAndTitle) {
web_app::FakeWebAppProvider* provider =
web_app::FakeWebAppProvider::Get(&profile_);
web_app::WebAppIconManager& icon_manager = provider->GetIconManager();
std::unique_ptr<web_app::WebApp> web_app = web_app::test::CreateWebApp();
const GURL web_app_url = web_app->start_url();
const web_app::AppId app_id = web_app->app_id();
web_app->SetName("Web App Title");
IconManagerWriteGeneratedIcons(icon_manager, app_id,
{{IconPurpose::MONOCHROME,
{web_app::icon_size::k16},
{SK_ColorTRANSPARENT}}});
web_app->SetDownloadedIconSizes(IconPurpose::MONOCHROME,
{web_app::icon_size::k16});
provider->GetRegistrarMutable().registry().emplace(app_id,
std::move(web_app));
IconManagerStartAndAwaitFaviconMonochrome(icon_manager, app_id);
provider->Start();
absl::optional<PlatformNotificationServiceImpl::WebAppIconAndTitle>
icon_and_title = service()->FindWebAppIconAndTitle(web_app_url);
ASSERT_TRUE(icon_and_title.has_value());
EXPECT_EQ(u"Web App Title", icon_and_title->title);
EXPECT_FALSE(icon_and_title->icon.isNull());
EXPECT_EQ(
SK_ColorTRANSPARENT,
icon_and_title->icon.GetRepresentation(1.0f).GetBitmap().getColor(0, 0));
}
#endif // !defined(OS_ANDROID)