1// Copyright (c) 2011 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/string_util.h"
8#include "base/utf_string_conversions.h"
9#include "chrome/browser/prefs/browser_prefs.h"
10#include "chrome/common/pref_names.h"
11#include "chrome/test/testing_pref_service.h"
12#include "content/common/desktop_notification_messages.h"
13
14// static
15const int MockBalloonCollection::kMockBalloonSpace = 5;
16
17// static
18std::string DesktopNotificationsTest::log_output_;
19
20MockBalloonCollection::MockBalloonCollection() {}
21
22MockBalloonCollection::~MockBalloonCollection() {}
23
24void MockBalloonCollection::Add(const Notification& notification,
25                                Profile* profile) {
26  // Swap in a logging proxy for the purpose of logging calls that
27  // would be made into javascript, then pass this down to the
28  // balloon collection.
29  Notification test_notification(
30      notification.origin_url(),
31      notification.content_url(),
32      notification.display_source(),
33      notification.replace_id(),
34      new LoggingNotificationProxy(notification.notification_id()));
35  BalloonCollectionImpl::Add(test_notification, profile);
36}
37
38bool MockBalloonCollection::HasSpace() const {
39  return count() < kMockBalloonSpace;
40}
41
42Balloon* MockBalloonCollection::MakeBalloon(const Notification& notification,
43                                            Profile* profile) {
44  // Start with a normal balloon but mock out the view.
45  Balloon* balloon = BalloonCollectionImpl::MakeBalloon(notification, profile);
46  balloon->set_view(new MockBalloonView(balloon));
47  balloons_.push_back(balloon);
48  return balloon;
49}
50
51void MockBalloonCollection::OnBalloonClosed(Balloon* source) {
52  std::deque<Balloon*>::iterator it;
53  for (it = balloons_.begin(); it != balloons_.end(); ++it) {
54    if (*it == source) {
55      balloons_.erase(it);
56      BalloonCollectionImpl::OnBalloonClosed(source);
57      break;
58    }
59  }
60}
61
62const BalloonCollection::Balloons& MockBalloonCollection::GetActiveBalloons() {
63  return balloons_;
64}
65
66int MockBalloonCollection::UppermostVerticalPosition() {
67  int min = 0;
68  std::deque<Balloon*>::iterator iter;
69  for (iter = balloons_.begin(); iter != balloons_.end(); ++iter) {
70    int pos = (*iter)->GetPosition().y();
71    if (iter == balloons_.begin() || pos < min)
72      min = pos;
73  }
74  return min;
75}
76
77DesktopNotificationsTest::DesktopNotificationsTest()
78    : ui_thread_(BrowserThread::UI, &message_loop_) {
79}
80
81DesktopNotificationsTest::~DesktopNotificationsTest() {
82}
83
84void DesktopNotificationsTest::SetUp() {
85  browser::RegisterLocalState(&local_state_);
86  profile_.reset(new TestingProfile());
87  balloon_collection_ = new MockBalloonCollection();
88  ui_manager_.reset(new NotificationUIManager(&local_state_));
89  ui_manager_->Initialize(balloon_collection_);
90  balloon_collection_->set_space_change_listener(ui_manager_.get());
91  service_.reset(new DesktopNotificationService(profile(), ui_manager_.get()));
92  log_output_.clear();
93}
94
95void DesktopNotificationsTest::TearDown() {
96  service_.reset(NULL);
97  ui_manager_.reset(NULL);
98  profile_.reset(NULL);
99}
100
101DesktopNotificationHostMsg_Show_Params
102DesktopNotificationsTest::StandardTestNotification() {
103  DesktopNotificationHostMsg_Show_Params params;
104  params.notification_id = 0;
105  params.origin = GURL("http://www.google.com");
106  params.is_html = false;
107  params.icon_url = GURL("/icon.png");
108  params.title = ASCIIToUTF16("Title");
109  params.body = ASCIIToUTF16("Text");
110  params.direction = WebKit::WebTextDirectionDefault;
111  return params;
112}
113
114TEST_F(DesktopNotificationsTest, TestShow) {
115  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
116  params.notification_id = 1;
117
118  EXPECT_TRUE(service_->ShowDesktopNotification(
119      params, 0, 0, DesktopNotificationService::PageNotification));
120  MessageLoopForUI::current()->RunAllPending();
121  EXPECT_EQ(1, balloon_collection_->count());
122
123  DesktopNotificationHostMsg_Show_Params params2;
124  params2.origin = GURL("http://www.google.com");
125  params2.is_html = true;
126  params2.contents_url = GURL("http://www.google.com/notification.html");
127  params2.notification_id = 2;
128
129  EXPECT_TRUE(service_->ShowDesktopNotification(
130      params2, 0, 0, DesktopNotificationService::PageNotification));
131  MessageLoopForUI::current()->RunAllPending();
132  EXPECT_EQ(2, balloon_collection_->count());
133
134  EXPECT_EQ("notification displayed\n"
135            "notification displayed\n",
136            log_output_);
137}
138
139TEST_F(DesktopNotificationsTest, TestClose) {
140  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
141  params.notification_id = 1;
142
143  // Request a notification; should open a balloon.
144  EXPECT_TRUE(service_->ShowDesktopNotification(
145      params, 0, 0, DesktopNotificationService::PageNotification));
146  MessageLoopForUI::current()->RunAllPending();
147  EXPECT_EQ(1, balloon_collection_->count());
148
149  // Close all the open balloons.
150  while (balloon_collection_->count() > 0) {
151    (*(balloon_collection_->GetActiveBalloons().begin()))->OnClose(true);
152  }
153
154  EXPECT_EQ("notification displayed\n"
155            "notification closed by user\n",
156            log_output_);
157}
158
159TEST_F(DesktopNotificationsTest, TestCancel) {
160  int process_id = 0;
161  int route_id = 0;
162  int notification_id = 1;
163
164  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
165  params.notification_id = notification_id;
166
167  // Request a notification; should open a balloon.
168  EXPECT_TRUE(service_->ShowDesktopNotification(
169      params, process_id, route_id,
170      DesktopNotificationService::PageNotification));
171  MessageLoopForUI::current()->RunAllPending();
172  EXPECT_EQ(1, balloon_collection_->count());
173
174  // Cancel the same notification
175  service_->CancelDesktopNotification(process_id,
176                                      route_id,
177                                      notification_id);
178  MessageLoopForUI::current()->RunAllPending();
179  // Verify that the balloon collection is now empty.
180  EXPECT_EQ(0, balloon_collection_->count());
181
182  EXPECT_EQ("notification displayed\n"
183            "notification closed by script\n",
184            log_output_);
185}
186
187#if defined(OS_WIN) || defined(TOOLKIT_VIEWS)
188TEST_F(DesktopNotificationsTest, TestPositioning) {
189  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
190  std::string expected_log;
191  // Create some toasts.  After each but the first, make sure there
192  // is a minimum separation between the toasts.
193  int last_top = 0;
194  for (int id = 0; id <= 3; ++id) {
195    params.notification_id = id;
196    EXPECT_TRUE(service_->ShowDesktopNotification(
197        params, 0, 0, DesktopNotificationService::PageNotification));
198    expected_log.append("notification displayed\n");
199    int top = balloon_collection_->UppermostVerticalPosition();
200    if (id > 0)
201      EXPECT_LE(top, last_top - balloon_collection_->MinHeight());
202    last_top = top;
203  }
204
205  EXPECT_EQ(expected_log, log_output_);
206}
207
208TEST_F(DesktopNotificationsTest, TestVariableSize) {
209  DesktopNotificationHostMsg_Show_Params params;
210  params.origin = GURL("http://long.google.com");
211  params.is_html = false;
212  params.icon_url = GURL("/icon.png");
213  params.title = ASCIIToUTF16("Really Really Really Really Really Really "
214                              "Really Really Really Really Really Really "
215                              "Really Really Really Really Really Really "
216                              "Really Long Title"),
217  params.body = ASCIIToUTF16("Text");
218  params.notification_id = 0;
219
220  std::string expected_log;
221  // Create some toasts.  After each but the first, make sure there
222  // is a minimum separation between the toasts.
223  EXPECT_TRUE(service_->ShowDesktopNotification(
224      params, 0, 0, DesktopNotificationService::PageNotification));
225  expected_log.append("notification displayed\n");
226
227  params.origin = GURL("http://short.google.com");
228  params.title = ASCIIToUTF16("Short title");
229  params.notification_id = 1;
230  EXPECT_TRUE(service_->ShowDesktopNotification(
231      params, 0, 0, DesktopNotificationService::PageNotification));
232  expected_log.append("notification displayed\n");
233
234  std::deque<Balloon*>& balloons = balloon_collection_->balloons();
235  std::deque<Balloon*>::iterator iter;
236  for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
237    if ((*iter)->notification().origin_url().host() == "long.google.com") {
238      EXPECT_GE((*iter)->GetViewSize().height(),
239                balloon_collection_->MinHeight());
240      EXPECT_LE((*iter)->GetViewSize().height(),
241                balloon_collection_->MaxHeight());
242    } else {
243      EXPECT_EQ((*iter)->GetViewSize().height(),
244                balloon_collection_->MinHeight());
245    }
246  }
247  EXPECT_EQ(expected_log, log_output_);
248}
249#endif
250
251TEST_F(DesktopNotificationsTest, TestQueueing) {
252  int process_id = 0;
253  int route_id = 0;
254
255  // Request lots of identical notifications.
256  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
257  const int kLotsOfToasts = 20;
258  for (int id = 1; id <= kLotsOfToasts; ++id) {
259    params.notification_id = id;
260    EXPECT_TRUE(service_->ShowDesktopNotification(
261        params, process_id, route_id,
262        DesktopNotificationService::PageNotification));
263  }
264  MessageLoopForUI::current()->RunAllPending();
265
266  // Build up an expected log of what should be happening.
267  std::string expected_log;
268  for (int i = 0; i < balloon_collection_->max_balloon_count(); ++i) {
269    expected_log.append("notification displayed\n");
270  }
271
272  // The max number that our balloon collection can hold should be
273  // shown.
274  EXPECT_EQ(balloon_collection_->max_balloon_count(),
275            balloon_collection_->count());
276  EXPECT_EQ(expected_log, log_output_);
277
278  // Cancel the notifications from the start; the balloon space should
279  // remain full.
280  {
281    int id;
282    for (id = 1;
283         id <= kLotsOfToasts - balloon_collection_->max_balloon_count();
284         ++id) {
285      service_->CancelDesktopNotification(process_id, route_id, id);
286      MessageLoopForUI::current()->RunAllPending();
287      expected_log.append("notification closed by script\n");
288      expected_log.append("notification displayed\n");
289      EXPECT_EQ(balloon_collection_->max_balloon_count(),
290                balloon_collection_->count());
291      EXPECT_EQ(expected_log, log_output_);
292    }
293
294    // Now cancel the rest.  It should empty the balloon space.
295    for (; id <= kLotsOfToasts; ++id) {
296      service_->CancelDesktopNotification(process_id, route_id, id);
297      expected_log.append("notification closed by script\n");
298      MessageLoopForUI::current()->RunAllPending();
299      EXPECT_EQ(expected_log, log_output_);
300    }
301  }
302
303  // Verify that the balloon collection is now empty.
304  EXPECT_EQ(0, balloon_collection_->count());
305}
306
307TEST_F(DesktopNotificationsTest, TestEarlyDestruction) {
308  // Create some toasts and then prematurely delete the notification service,
309  // just to make sure nothing crashes/leaks.
310  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
311  for (int id = 0; id <= 3; ++id) {
312    params.notification_id = id;
313    EXPECT_TRUE(service_->ShowDesktopNotification(
314        params, 0, 0, DesktopNotificationService::PageNotification));
315  }
316  service_.reset(NULL);
317}
318
319TEST_F(DesktopNotificationsTest, TestUserInputEscaping) {
320  // Create a test script with some HTML; assert that it doesn't get into the
321  // data:// URL that's produced for the balloon.
322  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
323  params.title = ASCIIToUTF16("<script>window.alert('uh oh');</script>");
324  params.body = ASCIIToUTF16("<i>this text is in italics</i>");
325  params.notification_id = 1;
326  EXPECT_TRUE(service_->ShowDesktopNotification(
327      params, 0, 0, DesktopNotificationService::PageNotification));
328
329  MessageLoopForUI::current()->RunAllPending();
330  EXPECT_EQ(1, balloon_collection_->count());
331  Balloon* balloon = (*balloon_collection_->balloons().begin());
332  GURL data_url = balloon->notification().content_url();
333  EXPECT_EQ(std::string::npos, data_url.spec().find("<script>"));
334  EXPECT_EQ(std::string::npos, data_url.spec().find("<i>"));
335  // URL-encoded versions of tags should also not be found.
336  EXPECT_EQ(std::string::npos, data_url.spec().find("%3cscript%3e"));
337  EXPECT_EQ(std::string::npos, data_url.spec().find("%3ci%3e"));
338}
339
340TEST_F(DesktopNotificationsTest, TestBoundingBox) {
341  // Create some notifications.
342  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
343  for (int id = 0; id <= 3; ++id) {
344    params.notification_id = id;
345    EXPECT_TRUE(service_->ShowDesktopNotification(
346        params, 0, 0, DesktopNotificationService::PageNotification));
347  }
348
349  gfx::Rect box = balloon_collection_->GetBalloonsBoundingBox();
350
351  // Try this for all positions.
352  BalloonCollection::PositionPreference pref;
353  for (pref = BalloonCollection::UPPER_RIGHT;
354       pref <= BalloonCollection::LOWER_LEFT;
355       pref = static_cast<BalloonCollection::PositionPreference>(pref + 1)) {
356    // Make sure each balloon's 4 corners are inside the box.
357    std::deque<Balloon*>& balloons = balloon_collection_->balloons();
358    std::deque<Balloon*>::iterator iter;
359    for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
360      int min_x = (*iter)->GetPosition().x();
361      int max_x = min_x + (*iter)->GetViewSize().width() - 1;
362      int min_y = (*iter)->GetPosition().y();
363      int max_y = min_y + (*iter)->GetViewSize().height() - 1;
364
365      EXPECT_TRUE(box.Contains(gfx::Point(min_x, min_y)));
366      EXPECT_TRUE(box.Contains(gfx::Point(min_x, max_y)));
367      EXPECT_TRUE(box.Contains(gfx::Point(max_x, min_y)));
368      EXPECT_TRUE(box.Contains(gfx::Point(max_x, max_y)));
369    }
370  }
371}
372
373TEST_F(DesktopNotificationsTest, TestPositionPreference) {
374  // Set position preference to lower right.
375  local_state_.SetInteger(prefs::kDesktopNotificationPosition,
376                          BalloonCollection::LOWER_RIGHT);
377
378  // Create some notifications.
379  DesktopNotificationHostMsg_Show_Params params = StandardTestNotification();
380  for (int id = 0; id <= 3; ++id) {
381    params.notification_id = id;
382    EXPECT_TRUE(service_->ShowDesktopNotification(
383        params, 0, 0, DesktopNotificationService::PageNotification));
384  }
385
386  std::deque<Balloon*>& balloons = balloon_collection_->balloons();
387  std::deque<Balloon*>::iterator iter;
388
389  // Check that they decrease in y-position (for MAC, with reversed
390  // coordinates, they should increase).
391  int last_y = -1;
392  int last_x = -1;
393
394  for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
395    int current_x = (*iter)->GetPosition().x();
396    int current_y = (*iter)->GetPosition().y();
397    if (last_x > 0)
398      EXPECT_EQ(last_x, current_x);
399
400    if (last_y > 0) {
401#if defined(OS_MACOSX)
402      EXPECT_GT(current_y, last_y);
403#else
404      EXPECT_LT(current_y, last_y);
405#endif
406    }
407
408    last_x = current_x;
409    last_y = current_y;
410  }
411
412  // Now change the position to upper right.  This should cause an immediate
413  // repositioning, and we check for the reverse ordering.
414  local_state_.SetInteger(prefs::kDesktopNotificationPosition,
415                          BalloonCollection::UPPER_RIGHT);
416  last_x = -1;
417  last_y = -1;
418
419  for (iter = balloons.begin(); iter != balloons.end(); ++iter) {
420    int current_x = (*iter)->GetPosition().x();
421    int current_y = (*iter)->GetPosition().y();
422
423    if (last_x > 0)
424      EXPECT_EQ(last_x, current_x);
425
426    if (last_y > 0) {
427#if defined(OS_MACOSX)
428      EXPECT_LT(current_y, last_y);
429#else
430      EXPECT_GT(current_y, last_y);
431#endif
432    }
433
434    last_x = current_x;
435    last_y = current_y;
436  }
437
438  // Now change the position to upper left.  Confirm that the X value for the
439  // balloons gets smaller.
440  local_state_.SetInteger(prefs::kDesktopNotificationPosition,
441                          BalloonCollection::UPPER_LEFT);
442
443  int current_x = (*balloons.begin())->GetPosition().x();
444  EXPECT_LT(current_x, last_x);
445}
446