// 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 "chrome/browser/ash/crosapi/message_center_ash.h"
#include <memory>
#include <optional>
#include <string>
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chromeos/crosapi/mojom/message_center.mojom.h"
#include "chromeos/crosapi/mojom/notification.mojom-shared.h"
#include "chromeos/crosapi/mojom/notification.mojom.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "ui/message_center/public/cpp/notifier_id.h"
#include "url/gurl.h"
using gfx::test::AreBitmapsEqual;
using gfx::test::AreImagesEqual;
namespace crosapi {
namespace {
// Creates an ash message center notification.
std::unique_ptr<message_center::Notification> CreateNotificationWithId(
const std::string& id) {
return std::make_unique<message_center::Notification>(
message_center::NOTIFICATION_TYPE_SIMPLE, id, u"title", u"message",
/*icon=*/ui::ImageModel(),
/*display_source=*/std::u16string(), GURL(), message_center::NotifierId(),
message_center::RichNotificationData(), /*delegate=*/nullptr);
}
class MojoDelegate : public mojom::NotificationDelegate {
public:
MojoDelegate() = default;
MojoDelegate(const MojoDelegate&) = delete;
MojoDelegate& operator=(const MojoDelegate&) = delete;
~MojoDelegate() override = default;
// crosapi::mojom::NotificationDelegate:
void OnNotificationClosed(bool by_user) override { ++closed_count_; }
void OnNotificationClicked() override { ++clicked_count_; }
void OnNotificationButtonClicked(
uint32_t button_index,
const std::optional<std::u16string>& reply) override {
++button_clicked_count_;
last_button_index_ = button_index;
}
void OnNotificationSettingsButtonClicked() override {
++settings_button_clicked_count_;
}
void OnNotificationDisabled() override { ++disabled_count_; }
// Public because this is test code.
int closed_count_ = 0;
int clicked_count_ = 0;
int button_clicked_count_ = 0;
uint32_t last_button_index_ = 0;
int settings_button_clicked_count_ = 0;
int disabled_count_ = 0;
mojo::Receiver<mojom::NotificationDelegate> receiver_{this};
};
class MessageCenterAshTest : public testing::Test {
public:
MessageCenterAshTest() = default;
MessageCenterAshTest(const MessageCenterAshTest&) = delete;
MessageCenterAshTest& operator=(const MessageCenterAshTest&) = delete;
~MessageCenterAshTest() override = default;
// testing::Test:
void SetUp() override {
message_center::MessageCenter::Initialize();
message_center_ash_ = std::make_unique<MessageCenterAsh>();
message_center_ash_->BindReceiver(
message_center_remote_.BindNewPipeAndPassReceiver());
}
void TearDown() override {
message_center_ash_.reset();
message_center::MessageCenter::Shutdown();
}
protected:
base::test::TaskEnvironment task_environment_;
mojo::Remote<mojom::MessageCenter> message_center_remote_;
std::unique_ptr<MessageCenterAsh> message_center_ash_;
};
TEST_F(MessageCenterAshTest, SerializationSimple) {
// Create a notification.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kSimple;
mojo_notification->id = "test_id";
mojo_notification->title = u"title";
mojo_notification->message = u"message";
mojo_notification->display_source = u"source";
mojo_notification->origin_url = GURL("http://example.com/");
mojo_notification->priority = 2;
mojo_notification->require_interaction = true;
base::Time now = base::Time::Now();
mojo_notification->timestamp = now;
mojo_notification->renotify = true;
mojo_notification->accessible_name = u"accessible_name";
mojo_notification->fullscreen_visibility =
mojom::FullscreenVisibility::kOverUser;
mojo_notification->notifier_id = mojom::NotifierId::New();
mojo_notification->notifier_id->type = mojom::NotifierType::kApplication;
mojo_notification->notifier_id->url = GURL("http://example.com/");
mojo_notification->notifier_id->id = "test_notifier_id";
mojo_notification->notifier_id->title = u"notifier_title";
mojo_notification->notifier_id->profile_id = "test_profile_id";
SkBitmap test_badge = gfx::test::CreateBitmap(1, 2);
mojo_notification->badge = gfx::ImageSkia::CreateFrom1xBitmap(test_badge);
SkBitmap test_icon = gfx::test::CreateBitmap(3, 4);
mojo_notification->icon = gfx::ImageSkia::CreateFrom1xBitmap(test_icon);
auto button1 = mojom::ButtonInfo::New();
button1->title = u"button1";
mojo_notification->buttons.push_back(std::move(button1));
auto button2 = mojom::ButtonInfo::New();
button2->title = u"button2";
button2->placeholder = std::make_optional(u"placeholder2");
mojo_notification->buttons.push_back(std::move(button2));
// Display the notification.
MojoDelegate mojo_delegate;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Notification exists and has correct fields.
auto* message_center = message_center::MessageCenter::Get();
message_center::Notification* ui_notification =
message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
EXPECT_EQ("test_id", ui_notification->id());
EXPECT_EQ(u"title", ui_notification->title());
EXPECT_EQ(u"message", ui_notification->message());
EXPECT_EQ(u"source", ui_notification->display_source());
EXPECT_EQ("http://example.com/", ui_notification->origin_url().spec());
EXPECT_EQ(2, ui_notification->priority());
EXPECT_TRUE(ui_notification->never_timeout());
EXPECT_EQ(now, ui_notification->timestamp());
EXPECT_TRUE(ui_notification->renotify());
EXPECT_EQ(u"accessible_name", ui_notification->accessible_name());
EXPECT_EQ(message_center::FullscreenVisibility::OVER_USER,
ui_notification->fullscreen_visibility());
EXPECT_EQ(ui_notification->notifier_id().type,
message_center::NotifierType::APPLICATION);
EXPECT_EQ(ui_notification->notifier_id().url, "http://example.com/");
EXPECT_EQ(ui_notification->notifier_id().id, "test_notifier_id");
EXPECT_EQ(ui_notification->notifier_id().title, u"notifier_title");
EXPECT_EQ(ui_notification->notifier_id().profile_id, "test_profile_id");
EXPECT_TRUE(
AreBitmapsEqual(test_badge, ui_notification->small_image().AsBitmap()));
EXPECT_TRUE(AreBitmapsEqual(
test_icon, *ui_notification->icon().Rasterize(nullptr).bitmap()));
ASSERT_EQ(2u, ui_notification->buttons().size());
EXPECT_EQ(u"button1", ui_notification->buttons()[0].title);
EXPECT_EQ(u"button2", ui_notification->buttons()[1].title);
EXPECT_EQ(u"placeholder2", ui_notification->buttons()[1].placeholder.value());
}
TEST_F(MessageCenterAshTest, SerializationImage) {
// Create a notification with an image.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kImage;
mojo_notification->id = "test_id";
SkBitmap test_image = gfx::test::CreateBitmap(5, 6);
mojo_notification->image = gfx::ImageSkia::CreateFrom1xBitmap(test_image);
// Display the notification.
MojoDelegate mojo_delegate;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Notification exists and has correct fields.
auto* message_center = message_center::MessageCenter::Get();
message_center::Notification* ui_notification =
message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
EXPECT_TRUE(AreBitmapsEqual(test_image, ui_notification->image().AsBitmap()));
}
TEST_F(MessageCenterAshTest, HighDpiImage) {
// Create a notification with an image.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kImage;
mojo_notification->id = "test_id";
// Create a high DPI image.
SkBitmap bitmap = gfx::test::CreateBitmap(2, 4);
gfx::ImageSkia high_dpi_image_skia =
gfx::ImageSkia::CreateFromBitmap(bitmap, 2.0f);
mojo_notification->image = high_dpi_image_skia;
// Display the notification.
MojoDelegate mojo_delegate;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Notification exists and has the high DPI image.
auto* message_center = message_center::MessageCenter::Get();
message_center::Notification* ui_notification =
message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
EXPECT_TRUE(AreImagesEqual(gfx::Image(high_dpi_image_skia),
ui_notification->image()));
}
TEST_F(MessageCenterAshTest, SerializationList) {
// Create a notification with some list items.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kList;
mojo_notification->id = "test_id";
auto item1 = mojom::NotificationItem::New();
item1->title = u"title1";
item1->message = u"message1";
mojo_notification->items.push_back(std::move(item1));
auto item2 = mojom::NotificationItem::New();
item2->title = u"title2";
item2->message = u"message2";
mojo_notification->items.push_back(std::move(item2));
// Display the notification.
MojoDelegate mojo_delegate;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Notification exists and has correct fields.
auto* message_center = message_center::MessageCenter::Get();
message_center::Notification* ui_notification =
message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
ASSERT_EQ(2u, ui_notification->items().size());
EXPECT_EQ(u"title1", ui_notification->items()[0].title());
EXPECT_EQ(u"message1", ui_notification->items()[0].message());
EXPECT_EQ(u"title2", ui_notification->items()[1].title());
EXPECT_EQ(u"message2", ui_notification->items()[1].message());
}
TEST_F(MessageCenterAshTest, SerializationProgress) {
// Create a notification with partial progress.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kProgress;
mojo_notification->id = "test_id";
mojo_notification->progress = 55;
mojo_notification->progress_status = u"status";
// Display the notification.
MojoDelegate mojo_delegate1;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate1.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Notification exists and has correct fields.
auto* message_center = message_center::MessageCenter::Get();
message_center::Notification* ui_notification =
message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
EXPECT_EQ(55, ui_notification->progress());
EXPECT_EQ(u"status", ui_notification->progress_status());
// Update progress past 100% by creating a new notification with the same ID.
mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kProgress;
mojo_notification->id = "test_id";
mojo_notification->progress = 101;
mojo_notification->progress_status = u"complete";
MojoDelegate mojo_delegate2;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate2.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
ui_notification = message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
// Progress was clamped to 100.
EXPECT_EQ(100, ui_notification->progress());
// Status was updated.
EXPECT_EQ(u"complete", ui_notification->progress_status());
}
// Regression test for https://crbug.com/1270544.
TEST_F(MessageCenterAshTest, DisplayNotificationCanUpdateWithoutClosing) {
// Display a progress notification.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kProgress;
mojo_notification->id = "test_id";
mojo_notification->progress = 55;
auto mojo_delegate1 = std::make_unique<MojoDelegate>();
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate1->receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Update the progress by creating a new notification with the same ID.
mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kProgress;
mojo_notification->id = "test_id";
mojo_notification->progress = 66;
auto mojo_delegate2 = std::make_unique<MojoDelegate>();
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate2->receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Destroy the first delegate, which destroys its mojo receiver. This
// simulates how Lacros updates notifications.
mojo_delegate1.reset();
message_center_remote_.FlushForTesting();
// Notification is still visible and has updated progress.
message_center::Notification* ui_notification =
message_center::MessageCenter::Get()->FindVisibleNotificationById(
"test_id");
ASSERT_TRUE(ui_notification);
EXPECT_EQ(66, ui_notification->progress());
}
TEST_F(MessageCenterAshTest, UserActions) {
// Build mojo notification for display.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kSimple;
mojo_notification->id = "test_id";
// Display the notification.
MojoDelegate mojo_delegate;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Notification exists.
auto* message_center = message_center::MessageCenter::Get();
message_center::Notification* ui_notification =
message_center->FindVisibleNotificationById("test_id");
ASSERT_TRUE(ui_notification);
// Simulate the user clicking on the notification body.
ui_notification->delegate()->Click(/*button_index=*/std::nullopt,
/*reply=*/std::nullopt);
mojo_delegate.receiver_.FlushForTesting();
EXPECT_EQ(1, mojo_delegate.clicked_count_);
// Simulate the user clicking on a notification button.
ui_notification->delegate()->Click(/*button_index=*/1,
/*reply=*/std::nullopt);
mojo_delegate.receiver_.FlushForTesting();
EXPECT_EQ(1, mojo_delegate.button_clicked_count_);
EXPECT_EQ(1u, mojo_delegate.last_button_index_);
// Simulate the user clicking on the settings button.
ui_notification->delegate()->SettingsClick();
mojo_delegate.receiver_.FlushForTesting();
EXPECT_EQ(1, mojo_delegate.settings_button_clicked_count_);
// Simulate the user disabling this notification.
ui_notification->delegate()->DisableNotification();
mojo_delegate.receiver_.FlushForTesting();
EXPECT_EQ(1, mojo_delegate.disabled_count_);
// Close the notification.
message_center_remote_->CloseNotification("test_id");
message_center_remote_.FlushForTesting();
EXPECT_FALSE(message_center->FindVisibleNotificationById("test_id"));
EXPECT_EQ(1, mojo_delegate.closed_count_);
}
TEST_F(MessageCenterAshTest, GetDisplayedNotifications) {
// Create an ash-side notification.
auto* message_center = message_center::MessageCenter::Get();
message_center->AddNotification(CreateNotificationWithId("id0"));
message_center->AddNotification(CreateNotificationWithId("id1"));
// Get the list of notifications.
base::test::TestFuture<const std::vector<std::string>&> future;
message_center_remote_->GetDisplayedNotifications(future.GetCallback());
// The notifications ids are returned. No particular order is specified.
EXPECT_THAT(future.Take(), testing::UnorderedElementsAre("id0", "id1"));
}
TEST_F(MessageCenterAshTest, NotificationsGroupByNotifierId) {
// Build mojo notification for display.
auto mojo_notification = mojom::Notification::New();
mojo_notification->type = mojom::NotificationType::kSimple;
mojo_notification->id = "test_id";
mojo_notification->origin_url = GURL("http://example.com/");
mojo_notification->notifier_id = mojom::NotifierId::New();
mojo_notification->notifier_id->type = mojom::NotifierType::kWebPage;
auto mojo_notification_2 = mojom::Notification::New();
mojo_notification_2->type = mojom::NotificationType::kSimple;
mojo_notification_2->id = "test_id_2";
mojo_notification_2->origin_url = GURL("http://example.com/");
mojo_notification_2->notifier_id = mojom::NotifierId::New();
mojo_notification_2->notifier_id->type = mojom::NotifierType::kWebPage;
// Display the notification.
MojoDelegate mojo_delegate;
message_center_remote_->DisplayNotification(
std::move(mojo_notification),
mojo_delegate.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// Display another notification from the same notifier_id.
MojoDelegate mojo_delegate2;
message_center_remote_->DisplayNotification(
std::move(mojo_notification_2),
mojo_delegate2.receiver_.BindNewPipeAndPassRemote());
message_center_remote_.FlushForTesting();
// There should only be a single popup since the new notification should be
// added to the existing notification as a grouped child.
EXPECT_EQ(
1u, message_center::MessageCenter::Get()->GetPopupNotifications().size());
}
} // namespace
} // namespace crosapi