1// Copyright 2020 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import <UserNotifications/UserNotifications.h> 6 7#include "base/bind.h" 8#include "base/mac/scoped_nsobject.h" 9#include "base/run_loop.h" 10#include "base/strings/utf_string_conversions.h" 11#include "base/test/bind.h" 12#include "chrome/browser/notifications/notification_platform_bridge_mac_unnotification.h" 13#include "chrome/browser/ui/cocoa/notifications/notification_constants_mac.h" 14#include "chrome/browser/ui/cocoa/notifications/unnotification_builder_mac.h" 15#include "chrome/browser/ui/cocoa/notifications/unnotification_response_builder_mac.h" 16#include "chrome/test/base/testing_browser_process.h" 17#include "chrome/test/base/testing_profile.h" 18#include "chrome/test/base/testing_profile_manager.h" 19#include "content/public/test/browser_task_environment.h" 20#include "testing/gtest/include/gtest/gtest.h" 21#include "testing/gtest_mac.h" 22#include "ui/message_center/public/cpp/notification.h" 23#include "url/gurl.h" 24 25// TODO(crbug/1146412): Move the mock classes to a separate file to avoid name 26// clashes. 27API_AVAILABLE(macosx(10.14)) 28@interface FakeNotification : NSObject 29@property(nonatomic, retain) UNNotificationRequest* request; 30@end 31 32@implementation FakeNotification 33@synthesize request; 34@end 35 36API_AVAILABLE(macosx(10.14)) 37@interface FakeUNUserNotificationCenter : NSObject 38- (instancetype)init; 39// Need to provide a nop implementation of setDelegate as it is 40// used during the setup of the bridge. 41- (void)setDelegate:(id<UNUserNotificationCenterDelegate>)delegate; 42- (void)removeAllDeliveredNotifications; 43- (void)setNotificationCategories:(NSSet<UNNotificationCategory*>*)categories; 44- (void)replaceContentForRequestWithIdentifier:(NSString*)requestIdentifier 45 replacementContent: 46 (UNMutableNotificationContent*)content 47 completionHandler: 48 (void (^)(NSError* _Nullable error)) 49 notificationDelivered; 50- (void)addNotificationRequest:(UNNotificationRequest*)request 51 withCompletionHandler:(void (^)(NSError* error))completionHandler; 52- (void)getDeliveredNotificationsWithCompletionHandler: 53 (void (^)(NSArray<UNNotification*>* notifications))completionHandler; 54- (void)getNotificationCategoriesWithCompletionHandler: 55 (void (^)(NSSet<UNNotificationCategory*>* categories))completionHandler; 56- (void)requestAuthorizationWithOptions:(UNAuthorizationOptions)options 57 completionHandler:(void (^)(BOOL granted, NSError* error)) 58 completionHandler; 59- (void)removeDeliveredNotificationsWithIdentifiers: 60 (NSArray<NSString*>*)identifiers; 61@end 62 63@implementation FakeUNUserNotificationCenter { 64 base::scoped_nsobject<NSMutableArray> _banners; 65 base::scoped_nsobject<NSSet> _categories; 66} 67 68- (instancetype)init { 69 if ((self = [super init])) { 70 _banners.reset([[NSMutableArray alloc] init]); 71 _categories.reset([[NSSet alloc] init]); 72 } 73 return self; 74} 75 76- (void)setDelegate:(id<UNUserNotificationCenterDelegate>)delegate { 77} 78 79- (void)removeAllDeliveredNotifications { 80 [_banners removeAllObjects]; 81} 82 83- (void)setNotificationCategories:(NSSet<UNNotificationCategory*>*)categories { 84 _categories.reset([categories copy]); 85} 86 87- (void)replaceContentForRequestWithIdentifier:(NSString*)requestIdentifier 88 replacementContent: 89 (UNMutableNotificationContent*)content 90 completionHandler: 91 (void (^)(NSError* _Nullable error)) 92 notificationDelivered { 93 UNNotificationRequest* request = 94 [UNNotificationRequest requestWithIdentifier:requestIdentifier 95 content:content 96 trigger:nil]; 97 base::scoped_nsobject<FakeNotification> notification( 98 [[FakeNotification alloc] init]); 99 [notification setRequest:request]; 100 [_banners addObject:notification]; 101 notificationDelivered(/*error=*/nil); 102} 103 104- (void)addNotificationRequest:(UNNotificationRequest*)request 105 withCompletionHandler:(void (^)(NSError* error))completionHandler { 106 base::scoped_nsobject<FakeNotification> notification( 107 [[FakeNotification alloc] init]); 108 [notification setRequest:request]; 109 [_banners addObject:notification]; 110 completionHandler(/*error=*/nil); 111} 112 113- (void)getDeliveredNotificationsWithCompletionHandler: 114 (void (^)(NSArray<UNNotification*>* notifications))completionHandler { 115 completionHandler([[_banners copy] autorelease]); 116} 117 118- (void)getNotificationCategoriesWithCompletionHandler: 119 (void (^)(NSSet<UNNotificationCategory*>* categories))completionHandler { 120 completionHandler([[_categories copy] autorelease]); 121} 122 123- (void)requestAuthorizationWithOptions:(UNAuthorizationOptions)options 124 completionHandler:(void (^)(BOOL granted, NSError* error)) 125 completionHandler { 126 completionHandler(/*granted=*/YES, /*error=*/nil); 127} 128 129- (void)removeDeliveredNotificationsWithIdentifiers: 130 (NSArray<NSString*>*)identifiers { 131 [_banners filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( 132 UNNotification* notification, 133 NSDictionary* bindings) { 134 NSString* toastId = [[[[notification request] content] userInfo] 135 objectForKey:notification_constants::kNotificationId]; 136 return ![identifiers containsObject:toastId]; 137 }]]; 138} 139@end 140 141using message_center::Notification; 142 143class UNNotificationPlatformBridgeMacTest : public testing::Test { 144 public: 145 UNNotificationPlatformBridgeMacTest() 146 : manager_(TestingBrowserProcess::GetGlobal()) {} 147 148 void SetUp() override { 149 ASSERT_TRUE(manager_.SetUp()); 150 profile_ = manager_.CreateTestingProfile("Moe"); 151 if (@available(macOS 10.14, *)) { 152 center_.reset([[FakeUNUserNotificationCenter alloc] init]); 153 bridge_ = std::make_unique<NotificationPlatformBridgeMacUNNotification>( 154 static_cast<UNUserNotificationCenter*>(center_.get())); 155 } 156 } 157 158 protected: 159 Notification CreateNotification(const std::string& notificationId = "id1") { 160 GURL url("https://gmail.com"); 161 162 Notification notification( 163 message_center::NOTIFICATION_TYPE_SIMPLE, notificationId, 164 base::UTF8ToUTF16("Title"), base::UTF8ToUTF16("Context"), gfx::Image(), 165 base::UTF8ToUTF16("Notifier's Name"), url, 166 message_center::NotifierId(url), message_center::RichNotificationData(), 167 base::MakeRefCounted<message_center::NotificationDelegate>()); 168 169 return notification; 170 } 171 172 API_AVAILABLE(macosx(10.14)) 173 base::scoped_nsobject<FakeUNUserNotificationCenter> center_; 174 API_AVAILABLE(macosx(10.14)) 175 std::unique_ptr<NotificationPlatformBridgeMacUNNotification> bridge_; 176 TestingProfile* profile_ = nullptr; 177 178 private: 179 content::BrowserTaskEnvironment task_environment_; 180 TestingProfileManager manager_; 181}; 182 183TEST_F(UNNotificationPlatformBridgeMacTest, TestDisplay) { 184 if (@available(macOS 10.14, *)) { 185 Notification notification = CreateNotification(); 186 187 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 188 notification, nullptr); 189 190 [center_ getDeliveredNotificationsWithCompletionHandler:^( 191 NSArray<UNNotification*>* _Nonnull notifications) { 192 ASSERT_EQ(1u, [notifications count]); 193 UNNotification* delivered_notification = [notifications objectAtIndex:0]; 194 UNNotificationContent* delivered_content = 195 [[delivered_notification request] content]; 196 EXPECT_NSEQ(@"Title", [delivered_content title]); 197 EXPECT_NSEQ(@"Context", [delivered_content body]); 198 EXPECT_NSEQ(@"gmail.com", [delivered_content subtitle]); 199 }]; 200 201 [center_ getNotificationCategoriesWithCompletionHandler:^( 202 NSSet<UNNotificationCategory*>* categories) { 203 EXPECT_EQ(1u, [categories count]); 204 }]; 205 } 206} 207 208TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationHasIcon) { 209 if (@available(macOS 10.14, *)) { 210 Notification notification = CreateNotification(); 211 212 SkBitmap icon; 213 icon.allocN32Pixels(64, 64); 214 icon.eraseARGB(255, 100, 150, 200); 215 notification.set_icon(gfx::Image::CreateFrom1xBitmap(icon)); 216 217 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 218 notification, nullptr); 219 220 [center_ getDeliveredNotificationsWithCompletionHandler:^( 221 NSArray<UNNotification*>* _Nonnull notifications) { 222 ASSERT_EQ(1u, [notifications count]); 223 UNNotification* delivered_notification = [notifications objectAtIndex:0]; 224 UNNotificationContent* delivered_content = 225 [[delivered_notification request] content]; 226 ASSERT_EQ(1u, [[delivered_content attachments] count]); 227 EXPECT_NSEQ(@"id1", [[[delivered_content attachments] objectAtIndex:0] 228 identifier]); 229 }]; 230 } 231} 232 233TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationNoIcon) { 234 if (@available(macOS 10.14, *)) { 235 Notification notification = CreateNotification(); 236 237 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 238 notification, nullptr); 239 240 [center_ getDeliveredNotificationsWithCompletionHandler:^( 241 NSArray<UNNotification*>* _Nonnull notifications) { 242 ASSERT_EQ(1u, [notifications count]); 243 UNNotification* delivered_notification = [notifications objectAtIndex:0]; 244 UNNotificationContent* delivered_content = 245 [[delivered_notification request] content]; 246 EXPECT_EQ(0u, [[delivered_content attachments] count]); 247 }]; 248 } 249} 250 251TEST_F(UNNotificationPlatformBridgeMacTest, TestCloseNotification) { 252 if (@available(macOS 10.14, *)) { 253 Notification notification = CreateNotification(); 254 255 [center_ getDeliveredNotificationsWithCompletionHandler:^( 256 NSArray<UNNotification*>* _Nonnull notifications) { 257 EXPECT_EQ(0u, [notifications count]); 258 }]; 259 260 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 261 notification, nullptr); 262 263 [center_ getDeliveredNotificationsWithCompletionHandler:^( 264 NSArray<UNNotification*>* _Nonnull notifications) { 265 EXPECT_EQ(1u, [notifications count]); 266 }]; 267 268 bridge_->Close(profile_, "id1"); 269 // RunLoop is used here to ensure that Close has finished executing before 270 // the notification count is checked below. Since Close executes 271 // asynchronous calls the order is not guaranteed by nature. 272 base::RunLoop().RunUntilIdle(); 273 [center_ getDeliveredNotificationsWithCompletionHandler:^( 274 NSArray<UNNotification*>* _Nonnull notifications) { 275 EXPECT_EQ(0u, [notifications count]); 276 }]; 277 } 278} 279 280TEST_F(UNNotificationPlatformBridgeMacTest, TestGetDisplayed) { 281 if (@available(macOS 10.14, *)) { 282 Notification notification = CreateNotification(); 283 284 [center_ getDeliveredNotificationsWithCompletionHandler:^( 285 NSArray<UNNotification*>* _Nonnull notifications) { 286 EXPECT_EQ(0u, [notifications count]); 287 }]; 288 289 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 290 notification, nullptr); 291 292 [center_ getDeliveredNotificationsWithCompletionHandler:^( 293 NSArray<UNNotification*>* _Nonnull notifications) { 294 EXPECT_EQ(1u, [notifications count]); 295 }]; 296 297 base::RunLoop run_loop; 298 int notification_count = -1; 299 bridge_->GetDisplayed( 300 profile_, 301 base::BindLambdaForTesting([&](std::set<std::string> notifications, 302 bool supports_synchronization) { 303 notification_count = notifications.size(); 304 run_loop.Quit(); 305 })); 306 run_loop.Run(); 307 EXPECT_EQ(1, notification_count); 308 } 309} 310 311TEST_F(UNNotificationPlatformBridgeMacTest, 312 TestGetDisplayedMultipleNotifications) { 313 if (@available(macOS 10.14, *)) { 314 Notification first_notification = CreateNotification("id1"); 315 316 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 317 first_notification, nullptr); 318 319 Notification second_notification = CreateNotification("id2"); 320 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 321 second_notification, nullptr); 322 323 base::RunLoop run_loop; 324 int notification_count = -1; 325 bridge_->GetDisplayed( 326 profile_, 327 base::BindLambdaForTesting([&](std::set<std::string> notifications, 328 bool supports_synchronization) { 329 notification_count = notifications.size(); 330 run_loop.Quit(); 331 })); 332 run_loop.Run(); 333 EXPECT_EQ(2, notification_count); 334 335 [center_ getNotificationCategoriesWithCompletionHandler:^( 336 NSSet<UNNotificationCategory*>* categories) { 337 EXPECT_EQ(2u, [categories count]); 338 }]; 339 } 340} 341 342TEST_F(UNNotificationPlatformBridgeMacTest, TestQuitRemovesNotifications) { 343 if (@available(macOS 10.14, *)) { 344 Notification notification = CreateNotification(); 345 346 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 347 notification, nullptr); 348 349 [center_ getDeliveredNotificationsWithCompletionHandler:^( 350 NSArray<UNNotification*>* _Nonnull notifications) { 351 EXPECT_EQ(1u, [notifications count]); 352 }]; 353 354 bridge_.reset(); 355 356 // The destructor of the bridge_ will call removeAllDeliveredNotifications. 357 [center_ getDeliveredNotificationsWithCompletionHandler:^( 358 NSArray<UNNotification*>* _Nonnull notifications) { 359 EXPECT_EQ(0u, [notifications count]); 360 }]; 361 } 362} 363 364TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationNoButtons) { 365 if (@available(macOS 10.14, *)) { 366 Notification notification = CreateNotification(); 367 368 notification.set_settings_button_handler( 369 message_center::SettingsButtonHandler::DELEGATE); 370 371 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 372 notification, nullptr); 373 374 [center_ getNotificationCategoriesWithCompletionHandler:^( 375 NSSet<UNNotificationCategory*>* categories) { 376 ASSERT_EQ(1u, [categories count]); 377 UNNotificationCategory* category = [categories anyObject]; 378 // If this selector from the private API is available the close button 379 // will be set in alernateAction, and the other buttons will be set in 380 // actions. Otherwise, all buttons will be setting in actions which causes 381 // the total count to differ by one. 382 if ([category respondsToSelector:@selector(alternateAction)]) { 383 ASSERT_EQ(1ul, [[category actions] count]); 384 EXPECT_NSEQ(@"Close", 385 [[category valueForKey:@"_alternateAction"] title]); 386 EXPECT_NSEQ(@"Settings", [[[category actions] lastObject] title]); 387 } else { 388 ASSERT_EQ(2ul, [[category actions] count]); 389 EXPECT_NSEQ(@"Close", [[[category actions] lastObject] title]); 390 EXPECT_NSEQ(@"Settings", [[[category actions] firstObject] title]); 391 } 392 }]; 393 } 394} 395 396TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationNoSettingsButton) { 397 if (@available(macOS 10.14, *)) { 398 Notification notification = CreateNotification(); 399 400 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 401 notification, nullptr); 402 403 [center_ getNotificationCategoriesWithCompletionHandler:^( 404 NSSet<UNNotificationCategory*>* categories) { 405 ASSERT_EQ(1u, [categories count]); 406 UNNotificationCategory* category = [categories anyObject]; 407 408 if ([category respondsToSelector:@selector(alternateAction)]) 409 EXPECT_EQ(0ul, [[category actions] count]); 410 else 411 EXPECT_EQ(1ul, [[category actions] count]); 412 }]; 413 } 414} 415 416TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationWithButtons) { 417 if (@available(macOS 10.14, *)) { 418 Notification notification = CreateNotification(); 419 420 notification.set_settings_button_handler( 421 message_center::SettingsButtonHandler::DELEGATE); 422 std::vector<message_center::ButtonInfo> buttons = { 423 message_center::ButtonInfo(base::UTF8ToUTF16("Button 1")), 424 message_center::ButtonInfo(base::UTF8ToUTF16("Button 2"))}; 425 notification.set_buttons(buttons); 426 427 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 428 notification, nullptr); 429 430 [center_ getNotificationCategoriesWithCompletionHandler:^( 431 NSSet<UNNotificationCategory*>* categories) { 432 ASSERT_EQ(1u, [categories count]); 433 UNNotificationCategory* category = [categories anyObject]; 434 435 if ([category respondsToSelector:@selector(alternateAction)]) { 436 EXPECT_NSEQ(@"Button 1", [[category actions][0] title]); 437 EXPECT_NSEQ(@"Button 2", [[category actions][1] title]); 438 EXPECT_EQ(3ul, [[category actions] count]); 439 } else { 440 EXPECT_NSEQ(@"Button 1", [[category actions][1] title]); 441 EXPECT_NSEQ(@"Button 2", [[category actions][2] title]); 442 EXPECT_EQ(4ul, [[category actions] count]); 443 } 444 }]; 445 } 446} 447 448TEST_F(UNNotificationPlatformBridgeMacTest, 449 TestNotificationCategoryIdentifier) { 450 if (@available(macOS 10.14, *)) { 451 Notification notification = CreateNotification(); 452 453 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 454 notification, nullptr); 455 456 [center_ getNotificationCategoriesWithCompletionHandler:^( 457 NSSet<UNNotificationCategory*>* categories) { 458 ASSERT_EQ(1u, [categories count]); 459 EXPECT_NSEQ(@"id1", [[categories anyObject] identifier]); 460 }]; 461 } 462} 463 464TEST_F(UNNotificationPlatformBridgeMacTest, TestCloseRemovesCategory) { 465 if (@available(macOS 10.14, *)) { 466 Notification first_notification = CreateNotification(); 467 468 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 469 first_notification, nullptr); 470 471 [center_ getNotificationCategoriesWithCompletionHandler:^( 472 NSSet<UNNotificationCategory*>* categories) { 473 EXPECT_EQ(1u, [categories count]); 474 }]; 475 476 bridge_->Close(profile_, "id1"); 477 // RunLoop is used here to ensure that Close has finished executing before 478 // the notification count is checked below. Since Close executes 479 // asynchronous calls the order is not guaranteed by nature. 480 base::RunLoop().RunUntilIdle(); 481 482 // Categories get updated during the next display call, so we call display 483 // to make sure that the category has been removed. 484 Notification second_notification = CreateNotification("id2"); 485 bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_, 486 second_notification, nullptr); 487 488 [center_ getNotificationCategoriesWithCompletionHandler:^( 489 NSSet<UNNotificationCategory*>* categories) { 490 ASSERT_EQ(1u, [categories count]); 491 EXPECT_NSEQ(@"id2", [[categories anyObject] identifier]); 492 }]; 493 } 494} 495