1 // Copyright (c) 2012 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 #include "chrome/browser/notifications/desktop_notifications_unittest.h" 6 7 #include "base/prefs/testing_pref_service.h" 8 #include "base/strings/string_util.h" 9 #include "base/strings/utf_string_conversions.h" 10 #include "chrome/browser/notifications/balloon_notification_ui_manager.h" 11 #include "chrome/browser/notifications/fake_balloon_view.h" 12 #include "chrome/browser/prefs/browser_prefs.h" 13 #include "chrome/common/pref_names.h" 14 #include "chrome/test/base/testing_browser_process.h" 15 #include "chrome/test/base/testing_profile.h" 16 #include "chrome/test/base/testing_profile_manager.h" 17 #include "content/public/common/show_desktop_notification_params.h" 18 #include "ui/base/ime/input_method_initializer.h" 19 #include "ui/message_center/message_center.h" 20 21 #if defined(USE_ASH) 22 #include "ash/shell.h" 23 #include "ash/test/test_shell_delegate.h" 24 #include "ui/aura/env.h" 25 #include "ui/aura/root_window.h" 26 #include "ui/compositor/scoped_animation_duration_scale_mode.h" 27 #include "ui/compositor/test/context_factories_for_test.h" 28 #endif 29 30 31 using content::BrowserThread; 32 33 // static 34 const int MockBalloonCollection::kMockBalloonSpace = 5; 35 36 // static 37 std::string DesktopNotificationsTest::log_output_; 38 39 MockBalloonCollection::MockBalloonCollection() {} 40 41 MockBalloonCollection::~MockBalloonCollection() {} 42 43 void MockBalloonCollection::Add(const Notification& notification, 44 Profile* profile) { 45 // Swap in a logging proxy for the purpose of logging calls that 46 // would be made into javascript, then pass this down to the 47 // balloon collection. 48 Notification test_notification( 49 notification.origin_url(), 50 notification.content_url(), 51 notification.display_source(), 52 notification.replace_id(), 53 new LoggingNotificationProxy(notification.notification_id())); 54 BalloonCollectionImpl::Add(test_notification, profile); 55 } 56 57 bool MockBalloonCollection::HasSpace() const { 58 return count() < kMockBalloonSpace; 59 } 60 61 Balloon* MockBalloonCollection::MakeBalloon(const Notification& notification, 62 Profile* profile) { 63 // Start with a normal balloon but mock out the view. 64 Balloon* balloon = BalloonCollectionImpl::MakeBalloon(notification, profile); 65 balloon->set_view(new FakeBalloonView(balloon)); 66 balloons_.push_back(balloon); 67 return balloon; 68 } 69 70 void MockBalloonCollection::OnBalloonClosed(Balloon* source) { 71 std::deque<Balloon*>::iterator it; 72 for (it = balloons_.begin(); it != balloons_.end(); ++it) { 73 if (*it == source) { 74 balloons_.erase(it); 75 BalloonCollectionImpl::OnBalloonClosed(source); 76 break; 77 } 78 } 79 } 80 81 const BalloonCollection::Balloons& MockBalloonCollection::GetActiveBalloons() { 82 return balloons_; 83 } 84 85 int MockBalloonCollection::UppermostVerticalPosition() { 86 int min = 0; 87 std::deque<Balloon*>::iterator iter; 88 for (iter = balloons_.begin(); iter != balloons_.end(); ++iter) { 89 int pos = (*iter)->GetPosition().y(); 90 if (iter == balloons_.begin() || pos < min) 91 min = pos; 92 } 93 return min; 94 } 95 96 DesktopNotificationsTest::DesktopNotificationsTest() 97 : ui_thread_(BrowserThread::UI, &message_loop_), 98 balloon_collection_(NULL) { 99 } 100 101 DesktopNotificationsTest::~DesktopNotificationsTest() { 102 } 103 104 void DesktopNotificationsTest::SetUp() { 105 ui::InitializeInputMethodForTesting(); 106 #if defined(USE_ASH) 107 ui::ScopedAnimationDurationScaleMode normal_duration_mode( 108 ui::ScopedAnimationDurationScaleMode::ZERO_DURATION); 109 // The message center is notmally initialized on |g_browser_process| which 110 // is not created for these tests. 111 message_center::MessageCenter::Initialize(); 112 // The ContextFactory must exist before any Compositors are created. 113 bool allow_test_contexts = true; 114 ui::InitializeContextFactoryForTests(allow_test_contexts); 115 // MockBalloonCollection retrieves information about the screen on creation. 116 // So it is necessary to make sure the desktop gets created first. 117 ash::Shell::CreateInstance(new ash::test::TestShellDelegate); 118 #endif 119 120 chrome::RegisterLocalState(local_state_.registry()); 121 profile_.reset(new TestingProfile()); 122 ui_manager_.reset(new BalloonNotificationUIManager(&local_state_)); 123 balloon_collection_ = new MockBalloonCollection(); 124 ui_manager_->SetBalloonCollection(balloon_collection_); 125 service_.reset(new DesktopNotificationService(profile(), ui_manager_.get())); 126 log_output_.clear(); 127 } 128 129 void DesktopNotificationsTest::TearDown() { 130 service_.reset(NULL); 131 ui_manager_.reset(NULL); 132 profile_.reset(NULL); 133 #if defined(USE_ASH) 134 ash::Shell::DeleteInstance(); 135 // The message center is notmally shutdown on |g_browser_process| which 136 // is not created for these tests. 137 message_center::MessageCenter::Shutdown(); 138 aura::Env::DeleteInstance(); 139 ui::TerminateContextFactoryForTests(); 140 #endif 141 ui::ShutdownInputMethodForTesting(); 142 } 143 144 content::ShowDesktopNotificationHostMsgParams 145 DesktopNotificationsTest::StandardTestNotification() { 146 content::ShowDesktopNotificationHostMsgParams params; 147 params.notification_id = 0; 148 params.origin = GURL("http://www.google.com"); 149 params.icon_url = GURL("/icon.png"); 150 params.title = ASCIIToUTF16("Title"); 151 params.body = ASCIIToUTF16("Text"); 152 params.direction = blink::WebTextDirectionDefault; 153 return params; 154 } 155 156 TEST_F(DesktopNotificationsTest, TestShow) { 157 content::ShowDesktopNotificationHostMsgParams params = 158 StandardTestNotification(); 159 params.notification_id = 1; 160 161 EXPECT_TRUE(service_->ShowDesktopNotification( 162 params, 0, 0, DesktopNotificationService::PageNotification)); 163 base::MessageLoopForUI::current()->RunUntilIdle(); 164 EXPECT_EQ(1, balloon_collection_->count()); 165 166 content::ShowDesktopNotificationHostMsgParams params2 = 167 StandardTestNotification(); 168 params2.notification_id = 2; 169 params2.origin = GURL("http://www.google.com"); 170 params2.body = ASCIIToUTF16("Text"); 171 172 EXPECT_TRUE(service_->ShowDesktopNotification( 173 params2, 0, 0, DesktopNotificationService::PageNotification)); 174 base::MessageLoopForUI::current()->RunUntilIdle(); 175 EXPECT_EQ(2, balloon_collection_->count()); 176 177 EXPECT_EQ("notification displayed\n" 178 "notification displayed\n", 179 log_output_); 180 } 181 182 TEST_F(DesktopNotificationsTest, TestClose) { 183 content::ShowDesktopNotificationHostMsgParams params = 184 StandardTestNotification(); 185 params.notification_id = 1; 186 187 // Request a notification; should open a balloon. 188 EXPECT_TRUE(service_->ShowDesktopNotification( 189 params, 0, 0, DesktopNotificationService::PageNotification)); 190 base::MessageLoopForUI::current()->RunUntilIdle(); 191 EXPECT_EQ(1, balloon_collection_->count()); 192 193 // Close all the open balloons. 194 while (balloon_collection_->count() > 0) { 195 (*(balloon_collection_->GetActiveBalloons().begin()))->OnClose(true); 196 } 197 198 EXPECT_EQ("notification displayed\n" 199 "notification closed by user\n", 200 log_output_); 201 } 202 203 TEST_F(DesktopNotificationsTest, TestCancel) { 204 int process_id = 0; 205 int route_id = 0; 206 int notification_id = 1; 207 208 content::ShowDesktopNotificationHostMsgParams params = 209 StandardTestNotification(); 210 params.notification_id = notification_id; 211 212 // Request a notification; should open a balloon. 213 EXPECT_TRUE(service_->ShowDesktopNotification( 214 params, process_id, route_id, 215 DesktopNotificationService::PageNotification)); 216 base::MessageLoopForUI::current()->RunUntilIdle(); 217 EXPECT_EQ(1, balloon_collection_->count()); 218 219 // Cancel the same notification 220 service_->CancelDesktopNotification(process_id, 221 route_id, 222 notification_id); 223 base::MessageLoopForUI::current()->RunUntilIdle(); 224 // Verify that the balloon collection is now empty. 225 EXPECT_EQ(0, balloon_collection_->count()); 226 227 EXPECT_EQ("notification displayed\n" 228 "notification closed by script\n", 229 log_output_); 230 } 231 232 #if defined(OS_WIN) || defined(TOOLKIT_VIEWS) 233 TEST_F(DesktopNotificationsTest, TestPositioning) { 234 content::ShowDesktopNotificationHostMsgParams params = 235 StandardTestNotification(); 236 std::string expected_log; 237 // Create some toasts. After each but the first, make sure there 238 // is a minimum separation between the toasts. 239 int last_top = 0; 240 for (int id = 0; id <= 3; ++id) { 241 params.notification_id = id; 242 EXPECT_TRUE(service_->ShowDesktopNotification( 243 params, 0, 0, DesktopNotificationService::PageNotification)); 244 expected_log.append("notification displayed\n"); 245 int top = balloon_collection_->UppermostVerticalPosition(); 246 if (id > 0) 247 EXPECT_LE(top, last_top - balloon_collection_->MinHeight()); 248 last_top = top; 249 } 250 251 EXPECT_EQ(expected_log, log_output_); 252 } 253 254 TEST_F(DesktopNotificationsTest, TestVariableSize) { 255 content::ShowDesktopNotificationHostMsgParams params; 256 params.origin = GURL("http://long.google.com"); 257 params.icon_url = GURL("/icon.png"); 258 params.title = ASCIIToUTF16("Really Really Really Really Really Really " 259 "Really Really Really Really Really Really " 260 "Really Really Really Really Really Really " 261 "Really Long Title"), 262 params.body = ASCIIToUTF16("Text"); 263 params.notification_id = 0; 264 265 std::string expected_log; 266 // Create some toasts. After each but the first, make sure there 267 // is a minimum separation between the toasts. 268 EXPECT_TRUE(service_->ShowDesktopNotification( 269 params, 0, 0, DesktopNotificationService::PageNotification)); 270 expected_log.append("notification displayed\n"); 271 272 params.origin = GURL("http://short.google.com"); 273 params.title = ASCIIToUTF16("Short title"); 274 params.notification_id = 1; 275 EXPECT_TRUE(service_->ShowDesktopNotification( 276 params, 0, 0, DesktopNotificationService::PageNotification)); 277 expected_log.append("notification displayed\n"); 278 279 std::deque<Balloon*>& balloons = balloon_collection_->balloons(); 280 std::deque<Balloon*>::iterator iter; 281 for (iter = balloons.begin(); iter != balloons.end(); ++iter) { 282 if ((*iter)->notification().origin_url().host() == "long.google.com") { 283 EXPECT_GE((*iter)->GetViewSize().height(), 284 balloon_collection_->MinHeight()); 285 EXPECT_LE((*iter)->GetViewSize().height(), 286 balloon_collection_->MaxHeight()); 287 } else { 288 EXPECT_EQ((*iter)->GetViewSize().height(), 289 balloon_collection_->MinHeight()); 290 } 291 } 292 EXPECT_EQ(expected_log, log_output_); 293 } 294 #endif 295 296 TEST_F(DesktopNotificationsTest, TestCancelByProfile) { 297 int process_id = 0; 298 int route_id = 0; 299 300 TestingBrowserProcess* browser_process = 301 TestingBrowserProcess::GetGlobal(); 302 TestingProfileManager profile_manager(browser_process); 303 ASSERT_TRUE(profile_manager.SetUp()); 304 305 TestingProfile* second_profile = 306 profile_manager.CreateTestingProfile("SecondTestingProfile"); 307 308 scoped_ptr<DesktopNotificationService> second_service( 309 new DesktopNotificationService(second_profile, ui_manager_.get())); 310 311 // Request lots of identical notifications. 312 content::ShowDesktopNotificationHostMsgParams params = 313 StandardTestNotification(); 314 params.notification_id = 1; 315 // Notice that the first one is the only one that doesn't use 316 // the second profile. 317 EXPECT_TRUE(service_->ShowDesktopNotification( 318 params, process_id, route_id, 319 DesktopNotificationService::PageNotification)); 320 321 // |kLotsOfToasts| must be large enough to trigger a resize of the underlying 322 // std::deque while we're clearing it. 323 const int kLotsOfToasts = 20; 324 for (int id = 2; id <= kLotsOfToasts; ++id) { 325 params.notification_id = id; 326 EXPECT_TRUE(second_service->ShowDesktopNotification( 327 params, process_id, route_id, 328 DesktopNotificationService::PageNotification)); 329 } 330 base::MessageLoopForUI::current()->RunUntilIdle(); 331 332 ui_manager_->CancelAllByProfile(second_profile); 333 334 // Verify that the balloon collection only contains the single 335 // notification from the first profile. 336 EXPECT_EQ(1, balloon_collection_->count()); 337 } 338 339 TEST_F(DesktopNotificationsTest, TestCancelBySourceOrigin) { 340 int process_id = 0; 341 int route_id = 0; 342 343 // Request lots of identical notifications. 344 content::ShowDesktopNotificationHostMsgParams params = 345 StandardTestNotification(); 346 347 // After the first, all the notifications are from attacker.com. 348 content::ShowDesktopNotificationHostMsgParams odd_params = 349 StandardTestNotification(); 350 odd_params.origin = GURL("attacker.com"); 351 352 // Show the only non-attacker.com notification. 353 params.notification_id = 1; 354 EXPECT_TRUE(service_->ShowDesktopNotification( 355 params, process_id, route_id, 356 DesktopNotificationService::PageNotification)); 357 358 // |kLotsOfToasts| must be large enough to trigger a resize of the underlying 359 // std::deque while we're clearing it. 360 const int kLotsOfToasts = 20; 361 for (int id = 2; id <= kLotsOfToasts; ++id) { 362 odd_params.notification_id = id; 363 EXPECT_TRUE(service_->ShowDesktopNotification( 364 odd_params, process_id, route_id, 365 DesktopNotificationService::PageNotification)); 366 } 367 base::MessageLoopForUI::current()->RunUntilIdle(); 368 369 ui_manager_->CancelAllBySourceOrigin(odd_params.origin); 370 371 // Verify that the balloon collection only contains the single 372 // notification which is not from the canceled origin. 373 EXPECT_EQ(1, balloon_collection_->count()); 374 } 375 376 TEST_F(DesktopNotificationsTest, TestQueueing) { 377 int process_id = 0; 378 int route_id = 0; 379 380 // Request lots of identical notifications. 381 content::ShowDesktopNotificationHostMsgParams params = 382 StandardTestNotification(); 383 const int kLotsOfToasts = 20; 384 for (int id = 1; id <= kLotsOfToasts; ++id) { 385 params.notification_id = id; 386 EXPECT_TRUE(service_->ShowDesktopNotification( 387 params, process_id, route_id, 388 DesktopNotificationService::PageNotification)); 389 } 390 base::MessageLoopForUI::current()->RunUntilIdle(); 391 392 // Build up an expected log of what should be happening. 393 std::string expected_log; 394 for (int i = 0; i < balloon_collection_->max_balloon_count(); ++i) { 395 expected_log.append("notification displayed\n"); 396 } 397 398 // The max number that our balloon collection can hold should be 399 // shown. 400 EXPECT_EQ(balloon_collection_->max_balloon_count(), 401 balloon_collection_->count()); 402 EXPECT_EQ(expected_log, log_output_); 403 404 // Cancel the notifications from the start; the balloon space should 405 // remain full. 406 { 407 int id; 408 for (id = 1; 409 id <= kLotsOfToasts - balloon_collection_->max_balloon_count(); 410 ++id) { 411 service_->CancelDesktopNotification(process_id, route_id, id); 412 base::MessageLoopForUI::current()->RunUntilIdle(); 413 expected_log.append("notification closed by script\n"); 414 expected_log.append("notification displayed\n"); 415 EXPECT_EQ(balloon_collection_->max_balloon_count(), 416 balloon_collection_->count()); 417 EXPECT_EQ(expected_log, log_output_); 418 } 419 420 // Now cancel the rest. It should empty the balloon space. 421 for (; id <= kLotsOfToasts; ++id) { 422 service_->CancelDesktopNotification(process_id, route_id, id); 423 expected_log.append("notification closed by script\n"); 424 base::MessageLoopForUI::current()->RunUntilIdle(); 425 EXPECT_EQ(expected_log, log_output_); 426 } 427 } 428 429 // Verify that the balloon collection is now empty. 430 EXPECT_EQ(0, balloon_collection_->count()); 431 } 432 433 TEST_F(DesktopNotificationsTest, TestEarlyDestruction) { 434 // Create some toasts and then prematurely delete the notification service, 435 // just to make sure nothing crashes/leaks. 436 content::ShowDesktopNotificationHostMsgParams params = 437 StandardTestNotification(); 438 for (int id = 0; id <= 3; ++id) { 439 params.notification_id = id; 440 EXPECT_TRUE(service_->ShowDesktopNotification( 441 params, 0, 0, DesktopNotificationService::PageNotification)); 442 } 443 service_.reset(NULL); 444 } 445 446 TEST_F(DesktopNotificationsTest, TestUserInputEscaping) { 447 // Create a test script with some HTML; assert that it doesn't get into the 448 // data:// URL that's produced for the balloon. 449 content::ShowDesktopNotificationHostMsgParams params = 450 StandardTestNotification(); 451 params.title = ASCIIToUTF16("<script>window.alert('uh oh');</script>"); 452 params.body = ASCIIToUTF16("<i>this text is in italics</i>"); 453 params.notification_id = 1; 454 EXPECT_TRUE(service_->ShowDesktopNotification( 455 params, 0, 0, DesktopNotificationService::PageNotification)); 456 457 base::MessageLoopForUI::current()->RunUntilIdle(); 458 EXPECT_EQ(1, balloon_collection_->count()); 459 Balloon* balloon = (*balloon_collection_->balloons().begin()); 460 GURL data_url = balloon->notification().content_url(); 461 EXPECT_EQ(std::string::npos, data_url.spec().find("<script>")); 462 EXPECT_EQ(std::string::npos, data_url.spec().find("<i>")); 463 // URL-encoded versions of tags should also not be found. 464 EXPECT_EQ(std::string::npos, data_url.spec().find("%3cscript%3e")); 465 EXPECT_EQ(std::string::npos, data_url.spec().find("%3ci%3e")); 466 } 467 468 TEST_F(DesktopNotificationsTest, TestBoundingBox) { 469 // Create some notifications. 470 content::ShowDesktopNotificationHostMsgParams params = 471 StandardTestNotification(); 472 for (int id = 0; id <= 3; ++id) { 473 params.notification_id = id; 474 EXPECT_TRUE(service_->ShowDesktopNotification( 475 params, 0, 0, DesktopNotificationService::PageNotification)); 476 } 477 478 gfx::Rect box = balloon_collection_->GetBalloonsBoundingBox(); 479 480 // Try this for all positions. 481 BalloonCollection::PositionPreference pref; 482 for (pref = BalloonCollection::UPPER_RIGHT; 483 pref <= BalloonCollection::LOWER_LEFT; 484 pref = static_cast<BalloonCollection::PositionPreference>(pref + 1)) { 485 // Make sure each balloon's 4 corners are inside the box. 486 std::deque<Balloon*>& balloons = balloon_collection_->balloons(); 487 std::deque<Balloon*>::iterator iter; 488 for (iter = balloons.begin(); iter != balloons.end(); ++iter) { 489 int min_x = (*iter)->GetPosition().x(); 490 int max_x = min_x + (*iter)->GetViewSize().width() - 1; 491 int min_y = (*iter)->GetPosition().y(); 492 int max_y = min_y + (*iter)->GetViewSize().height() - 1; 493 494 EXPECT_TRUE(box.Contains(gfx::Point(min_x, min_y))); 495 EXPECT_TRUE(box.Contains(gfx::Point(min_x, max_y))); 496 EXPECT_TRUE(box.Contains(gfx::Point(max_x, min_y))); 497 EXPECT_TRUE(box.Contains(gfx::Point(max_x, max_y))); 498 } 499 } 500 } 501 502 TEST_F(DesktopNotificationsTest, TestPositionPreference) { 503 // Set position preference to lower right. 504 local_state_.SetInteger(prefs::kDesktopNotificationPosition, 505 BalloonCollection::LOWER_RIGHT); 506 507 // Create some notifications. 508 content::ShowDesktopNotificationHostMsgParams params = 509 StandardTestNotification(); 510 for (int id = 0; id <= 3; ++id) { 511 params.notification_id = id; 512 EXPECT_TRUE(service_->ShowDesktopNotification( 513 params, 0, 0, DesktopNotificationService::PageNotification)); 514 } 515 516 std::deque<Balloon*>& balloons = balloon_collection_->balloons(); 517 std::deque<Balloon*>::iterator iter; 518 519 // Check that they decrease in y-position (for MAC, with reversed 520 // coordinates, they should increase). 521 int last_y = -1; 522 int last_x = -1; 523 524 for (iter = balloons.begin(); iter != balloons.end(); ++iter) { 525 int current_x = (*iter)->GetPosition().x(); 526 int current_y = (*iter)->GetPosition().y(); 527 if (last_x > 0) 528 EXPECT_EQ(last_x, current_x); 529 530 if (last_y > 0) { 531 #if defined(OS_MACOSX) 532 EXPECT_GT(current_y, last_y); 533 #else 534 EXPECT_LT(current_y, last_y); 535 #endif 536 } 537 538 last_x = current_x; 539 last_y = current_y; 540 } 541 542 // Now change the position to upper right. This should cause an immediate 543 // repositioning, and we check for the reverse ordering. 544 local_state_.SetInteger(prefs::kDesktopNotificationPosition, 545 BalloonCollection::UPPER_RIGHT); 546 last_x = -1; 547 last_y = -1; 548 549 for (iter = balloons.begin(); iter != balloons.end(); ++iter) { 550 int current_x = (*iter)->GetPosition().x(); 551 int current_y = (*iter)->GetPosition().y(); 552 553 if (last_x > 0) 554 EXPECT_EQ(last_x, current_x); 555 556 if (last_y > 0) { 557 #if defined(OS_MACOSX) 558 EXPECT_LT(current_y, last_y); 559 #else 560 EXPECT_GT(current_y, last_y); 561 #endif 562 } 563 564 last_x = current_x; 565 last_y = current_y; 566 } 567 568 // Now change the position to upper left. Confirm that the X value for the 569 // balloons gets smaller. 570 local_state_.SetInteger(prefs::kDesktopNotificationPosition, 571 BalloonCollection::UPPER_LEFT); 572 573 int current_x = (*balloons.begin())->GetPosition().x(); 574 EXPECT_LT(current_x, last_x); 575 } 576