// Copyright (c) 2012 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 "chrome/browser/notifications/desktop_notifications_unittest.h" #include "base/prefs/testing_pref_service.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/notifications/balloon_notification_ui_manager.h" #include "chrome/browser/notifications/fake_balloon_view.h" #include "chrome/browser/prefs/browser_prefs.h" #include "chrome/common/pref_names.h" #include "chrome/test/base/testing_browser_process.h" #include "chrome/test/base/testing_profile.h" #include "chrome/test/base/testing_profile_manager.h" #include "content/public/common/show_desktop_notification_params.h" #include "ui/base/ime/input_method_initializer.h" #include "ui/message_center/message_center.h" #if defined(USE_ASH) #include "ash/shell.h" #include "ash/test/test_shell_delegate.h" #include "ui/aura/env.h" #include "ui/aura/root_window.h" #include "ui/compositor/scoped_animation_duration_scale_mode.h" #include "ui/compositor/test/context_factories_for_test.h" #endif using content::BrowserThread; // static const int MockBalloonCollection::kMockBalloonSpace = 5; // static std::string DesktopNotificationsTest::log_output_; MockBalloonCollection::MockBalloonCollection() {} MockBalloonCollection::~MockBalloonCollection() {} void MockBalloonCollection::Add(const Notification& notification, Profile* profile) { // Swap in a logging proxy for the purpose of logging calls that // would be made into javascript, then pass this down to the // balloon collection. Notification test_notification( notification.origin_url(), notification.content_url(), notification.display_source(), notification.replace_id(), new LoggingNotificationProxy(notification.notification_id())); BalloonCollectionImpl::Add(test_notification, profile); } bool MockBalloonCollection::HasSpace() const { return count() < kMockBalloonSpace; } Balloon* MockBalloonCollection::MakeBalloon(const Notification& notification, Profile* profile) { // Start with a normal balloon but mock out the view. Balloon* balloon = BalloonCollectionImpl::MakeBalloon(notification, profile); balloon->set_view(new FakeBalloonView(balloon)); balloons_.push_back(balloon); return balloon; } void MockBalloonCollection::OnBalloonClosed(Balloon* source) { std::deque<Balloon*>::iterator it; for (it = balloons_.begin(); it != balloons_.end(); ++it) { if (*it == source) { balloons_.erase(it); BalloonCollectionImpl::OnBalloonClosed(source); break; } } } const BalloonCollection::Balloons& MockBalloonCollection::GetActiveBalloons() { return balloons_; } int MockBalloonCollection::UppermostVerticalPosition() { int min = 0; std::deque<Balloon*>::iterator iter; for (iter = balloons_.begin(); iter != balloons_.end(); ++iter) { int pos = (*iter)->GetPosition().y(); if (iter == balloons_.begin() || pos < min) min = pos; } return min; } DesktopNotificationsTest::DesktopNotificationsTest() : ui_thread_(BrowserThread::UI, &message_loop_), balloon_collection_(NULL) { } DesktopNotificationsTest::~DesktopNotificationsTest() { } void DesktopNotificationsTest::SetUp() { ui::InitializeInputMethodForTesting(); #if defined(USE_ASH) ui::ScopedAnimationDurationScaleMode normal_duration_mode( ui::ScopedAnimationDurationScaleMode::ZERO_DURATION); // The message center is notmally initialized on |g_browser_process| which // is not created for these tests. message_center::MessageCenter::Initialize(); // The ContextFactory must exist before any Compositors are created. bool allow_test_contexts = true; ui::InitializeContextFactoryForTests(allow_test_contexts); // MockBalloonCollection retrieves information about the screen on creation. // So it is necessary to make sure the desktop gets created first. ash::Shell::CreateInstance(new ash::test::TestShellDelegate); #endif chrome::RegisterLocalState(local_state_.registry()); profile_.reset(new TestingProfile()); ui_manager_.reset(new BalloonNotificationUIManager(&local_state_)); balloon_collection_ = new MockBalloonCollection(); ui_manager_->SetBalloonCollection(balloon_collection_); service_.reset(new DesktopNotificationService(profile(), ui_manager_.get())); log_output_.clear(); } void DesktopNotificationsTest::TearDown() { service_.reset(NULL); ui_manager_.reset(NULL); profile_.reset(NULL); #if defined(USE_ASH) ash::Shell::DeleteInstance(); // The message center is notmally shutdown on |g_browser_process| which // is not created for these tests. message_center::MessageCenter::Shutdown(); aura::Env::DeleteInstance(); ui::TerminateContextFactoryForTests(); #endif ui::ShutdownInputMethodForTesting(); } content::ShowDesktopNotificationHostMsgParams DesktopNotificationsTest::StandardTestNotification() { content::ShowDesktopNotificationHostMsgParams params; params.notification_id = 0; params.origin = GURL("http://www.google.com"); params.icon_url = GURL("/icon.png"); params.title = ASCIIToUTF16("Title"); params.body = ASCIIToUTF16("Text"); params.direction = blink::WebTextDirectionDefault; return params; } TEST_F(DesktopNotificationsTest, TestShow) { content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); params.notification_id = 1; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); base::MessageLoopForUI::current()->RunUntilIdle(); EXPECT_EQ(1, balloon_collection_->count()); content::ShowDesktopNotificationHostMsgParams params2 = StandardTestNotification(); params2.notification_id = 2; params2.origin = GURL("http://www.google.com"); params2.body = ASCIIToUTF16("Text"); EXPECT_TRUE(service_->ShowDesktopNotification( params2, 0, 0, DesktopNotificationService::PageNotification)); base::MessageLoopForUI::current()->RunUntilIdle(); EXPECT_EQ(2, balloon_collection_->count()); EXPECT_EQ("notification displayed\n" "notification displayed\n", log_output_); } TEST_F(DesktopNotificationsTest, TestClose) { content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); params.notification_id = 1; // Request a notification; should open a balloon. EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); base::MessageLoopForUI::current()->RunUntilIdle(); EXPECT_EQ(1, balloon_collection_->count()); // Close all the open balloons. while (balloon_collection_->count() > 0) { (*(balloon_collection_->GetActiveBalloons().begin()))->OnClose(true); } EXPECT_EQ("notification displayed\n" "notification closed by user\n", log_output_); } TEST_F(DesktopNotificationsTest, TestCancel) { int process_id = 0; int route_id = 0; int notification_id = 1; content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); params.notification_id = notification_id; // Request a notification; should open a balloon. EXPECT_TRUE(service_->ShowDesktopNotification( params, process_id, route_id, DesktopNotificationService::PageNotification)); base::MessageLoopForUI::current()->RunUntilIdle(); EXPECT_EQ(1, balloon_collection_->count()); // Cancel the same notification service_->CancelDesktopNotification(process_id, route_id, notification_id); base::MessageLoopForUI::current()->RunUntilIdle(); // Verify that the balloon collection is now empty. EXPECT_EQ(0, balloon_collection_->count()); EXPECT_EQ("notification displayed\n" "notification closed by script\n", log_output_); } #if defined(OS_WIN) || defined(TOOLKIT_VIEWS) TEST_F(DesktopNotificationsTest, TestPositioning) { content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); std::string expected_log; // Create some toasts. After each but the first, make sure there // is a minimum separation between the toasts. int last_top = 0; for (int id = 0; id <= 3; ++id) { params.notification_id = id; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); expected_log.append("notification displayed\n"); int top = balloon_collection_->UppermostVerticalPosition(); if (id > 0) EXPECT_LE(top, last_top - balloon_collection_->MinHeight()); last_top = top; } EXPECT_EQ(expected_log, log_output_); } TEST_F(DesktopNotificationsTest, TestVariableSize) { content::ShowDesktopNotificationHostMsgParams params; params.origin = GURL("http://long.google.com"); params.icon_url = GURL("/icon.png"); params.title = ASCIIToUTF16("Really Really Really Really Really Really " "Really Really Really Really Really Really " "Really Really Really Really Really Really " "Really Long Title"), params.body = ASCIIToUTF16("Text"); params.notification_id = 0; std::string expected_log; // Create some toasts. After each but the first, make sure there // is a minimum separation between the toasts. EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); expected_log.append("notification displayed\n"); params.origin = GURL("http://short.google.com"); params.title = ASCIIToUTF16("Short title"); params.notification_id = 1; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); expected_log.append("notification displayed\n"); std::deque<Balloon*>& balloons = balloon_collection_->balloons(); std::deque<Balloon*>::iterator iter; for (iter = balloons.begin(); iter != balloons.end(); ++iter) { if ((*iter)->notification().origin_url().host() == "long.google.com") { EXPECT_GE((*iter)->GetViewSize().height(), balloon_collection_->MinHeight()); EXPECT_LE((*iter)->GetViewSize().height(), balloon_collection_->MaxHeight()); } else { EXPECT_EQ((*iter)->GetViewSize().height(), balloon_collection_->MinHeight()); } } EXPECT_EQ(expected_log, log_output_); } #endif TEST_F(DesktopNotificationsTest, TestCancelByProfile) { int process_id = 0; int route_id = 0; TestingBrowserProcess* browser_process = TestingBrowserProcess::GetGlobal(); TestingProfileManager profile_manager(browser_process); ASSERT_TRUE(profile_manager.SetUp()); TestingProfile* second_profile = profile_manager.CreateTestingProfile("SecondTestingProfile"); scoped_ptr<DesktopNotificationService> second_service( new DesktopNotificationService(second_profile, ui_manager_.get())); // Request lots of identical notifications. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); params.notification_id = 1; // Notice that the first one is the only one that doesn't use // the second profile. EXPECT_TRUE(service_->ShowDesktopNotification( params, process_id, route_id, DesktopNotificationService::PageNotification)); // |kLotsOfToasts| must be large enough to trigger a resize of the underlying // std::deque while we're clearing it. const int kLotsOfToasts = 20; for (int id = 2; id <= kLotsOfToasts; ++id) { params.notification_id = id; EXPECT_TRUE(second_service->ShowDesktopNotification( params, process_id, route_id, DesktopNotificationService::PageNotification)); } base::MessageLoopForUI::current()->RunUntilIdle(); ui_manager_->CancelAllByProfile(second_profile); // Verify that the balloon collection only contains the single // notification from the first profile. EXPECT_EQ(1, balloon_collection_->count()); } TEST_F(DesktopNotificationsTest, TestCancelBySourceOrigin) { int process_id = 0; int route_id = 0; // Request lots of identical notifications. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); // After the first, all the notifications are from attacker.com. content::ShowDesktopNotificationHostMsgParams odd_params = StandardTestNotification(); odd_params.origin = GURL("attacker.com"); // Show the only non-attacker.com notification. params.notification_id = 1; EXPECT_TRUE(service_->ShowDesktopNotification( params, process_id, route_id, DesktopNotificationService::PageNotification)); // |kLotsOfToasts| must be large enough to trigger a resize of the underlying // std::deque while we're clearing it. const int kLotsOfToasts = 20; for (int id = 2; id <= kLotsOfToasts; ++id) { odd_params.notification_id = id; EXPECT_TRUE(service_->ShowDesktopNotification( odd_params, process_id, route_id, DesktopNotificationService::PageNotification)); } base::MessageLoopForUI::current()->RunUntilIdle(); ui_manager_->CancelAllBySourceOrigin(odd_params.origin); // Verify that the balloon collection only contains the single // notification which is not from the canceled origin. EXPECT_EQ(1, balloon_collection_->count()); } TEST_F(DesktopNotificationsTest, TestQueueing) { int process_id = 0; int route_id = 0; // Request lots of identical notifications. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); const int kLotsOfToasts = 20; for (int id = 1; id <= kLotsOfToasts; ++id) { params.notification_id = id; EXPECT_TRUE(service_->ShowDesktopNotification( params, process_id, route_id, DesktopNotificationService::PageNotification)); } base::MessageLoopForUI::current()->RunUntilIdle(); // Build up an expected log of what should be happening. std::string expected_log; for (int i = 0; i < balloon_collection_->max_balloon_count(); ++i) { expected_log.append("notification displayed\n"); } // The max number that our balloon collection can hold should be // shown. EXPECT_EQ(balloon_collection_->max_balloon_count(), balloon_collection_->count()); EXPECT_EQ(expected_log, log_output_); // Cancel the notifications from the start; the balloon space should // remain full. { int id; for (id = 1; id <= kLotsOfToasts - balloon_collection_->max_balloon_count(); ++id) { service_->CancelDesktopNotification(process_id, route_id, id); base::MessageLoopForUI::current()->RunUntilIdle(); expected_log.append("notification closed by script\n"); expected_log.append("notification displayed\n"); EXPECT_EQ(balloon_collection_->max_balloon_count(), balloon_collection_->count()); EXPECT_EQ(expected_log, log_output_); } // Now cancel the rest. It should empty the balloon space. for (; id <= kLotsOfToasts; ++id) { service_->CancelDesktopNotification(process_id, route_id, id); expected_log.append("notification closed by script\n"); base::MessageLoopForUI::current()->RunUntilIdle(); EXPECT_EQ(expected_log, log_output_); } } // Verify that the balloon collection is now empty. EXPECT_EQ(0, balloon_collection_->count()); } TEST_F(DesktopNotificationsTest, TestEarlyDestruction) { // Create some toasts and then prematurely delete the notification service, // just to make sure nothing crashes/leaks. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); for (int id = 0; id <= 3; ++id) { params.notification_id = id; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); } service_.reset(NULL); } TEST_F(DesktopNotificationsTest, TestUserInputEscaping) { // Create a test script with some HTML; assert that it doesn't get into the // data:// URL that's produced for the balloon. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); params.title = ASCIIToUTF16("<script>window.alert('uh oh');</script>"); params.body = ASCIIToUTF16("<i>this text is in italics</i>"); params.notification_id = 1; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); base::MessageLoopForUI::current()->RunUntilIdle(); EXPECT_EQ(1, balloon_collection_->count()); Balloon* balloon = (*balloon_collection_->balloons().begin()); GURL data_url = balloon->notification().content_url(); EXPECT_EQ(std::string::npos, data_url.spec().find("<script>")); EXPECT_EQ(std::string::npos, data_url.spec().find("<i>")); // URL-encoded versions of tags should also not be found. EXPECT_EQ(std::string::npos, data_url.spec().find("%3cscript%3e")); EXPECT_EQ(std::string::npos, data_url.spec().find("%3ci%3e")); } TEST_F(DesktopNotificationsTest, TestBoundingBox) { // Create some notifications. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); for (int id = 0; id <= 3; ++id) { params.notification_id = id; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); } gfx::Rect box = balloon_collection_->GetBalloonsBoundingBox(); // Try this for all positions. BalloonCollection::PositionPreference pref; for (pref = BalloonCollection::UPPER_RIGHT; pref <= BalloonCollection::LOWER_LEFT; pref = static_cast<BalloonCollection::PositionPreference>(pref + 1)) { // Make sure each balloon's 4 corners are inside the box. std::deque<Balloon*>& balloons = balloon_collection_->balloons(); std::deque<Balloon*>::iterator iter; for (iter = balloons.begin(); iter != balloons.end(); ++iter) { int min_x = (*iter)->GetPosition().x(); int max_x = min_x + (*iter)->GetViewSize().width() - 1; int min_y = (*iter)->GetPosition().y(); int max_y = min_y + (*iter)->GetViewSize().height() - 1; EXPECT_TRUE(box.Contains(gfx::Point(min_x, min_y))); EXPECT_TRUE(box.Contains(gfx::Point(min_x, max_y))); EXPECT_TRUE(box.Contains(gfx::Point(max_x, min_y))); EXPECT_TRUE(box.Contains(gfx::Point(max_x, max_y))); } } } TEST_F(DesktopNotificationsTest, TestPositionPreference) { // Set position preference to lower right. local_state_.SetInteger(prefs::kDesktopNotificationPosition, BalloonCollection::LOWER_RIGHT); // Create some notifications. content::ShowDesktopNotificationHostMsgParams params = StandardTestNotification(); for (int id = 0; id <= 3; ++id) { params.notification_id = id; EXPECT_TRUE(service_->ShowDesktopNotification( params, 0, 0, DesktopNotificationService::PageNotification)); } std::deque<Balloon*>& balloons = balloon_collection_->balloons(); std::deque<Balloon*>::iterator iter; // Check that they decrease in y-position (for MAC, with reversed // coordinates, they should increase). int last_y = -1; int last_x = -1; for (iter = balloons.begin(); iter != balloons.end(); ++iter) { int current_x = (*iter)->GetPosition().x(); int current_y = (*iter)->GetPosition().y(); if (last_x > 0) EXPECT_EQ(last_x, current_x); if (last_y > 0) { #if defined(OS_MACOSX) EXPECT_GT(current_y, last_y); #else EXPECT_LT(current_y, last_y); #endif } last_x = current_x; last_y = current_y; } // Now change the position to upper right. This should cause an immediate // repositioning, and we check for the reverse ordering. local_state_.SetInteger(prefs::kDesktopNotificationPosition, BalloonCollection::UPPER_RIGHT); last_x = -1; last_y = -1; for (iter = balloons.begin(); iter != balloons.end(); ++iter) { int current_x = (*iter)->GetPosition().x(); int current_y = (*iter)->GetPosition().y(); if (last_x > 0) EXPECT_EQ(last_x, current_x); if (last_y > 0) { #if defined(OS_MACOSX) EXPECT_LT(current_y, last_y); #else EXPECT_GT(current_y, last_y); #endif } last_x = current_x; last_y = current_y; } // Now change the position to upper left. Confirm that the X value for the // balloons gets smaller. local_state_.SetInteger(prefs::kDesktopNotificationPosition, BalloonCollection::UPPER_LEFT); int current_x = (*balloons.begin())->GetPosition().x(); EXPECT_LT(current_x, last_x); }