1 // Copyright 2014 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.
5 #include "ui/message_center/views/notification_view.h"
7 #include "base/memory/scoped_ptr.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "testing/gtest/include/gtest/gtest.h"
10 #include "third_party/skia/include/core/SkBitmap.h"
11 #include "third_party/skia/include/core/SkCanvas.h"
12 #include "third_party/skia/include/core/SkColor.h"
13 #include "ui/events/event_processor.h"
14 #include "ui/events/event_utils.h"
15 #include "ui/gfx/canvas.h"
16 #include "ui/gfx/geometry/rect.h"
17 #include "ui/gfx/geometry/size.h"
18 #include "ui/gfx/image/image.h"
19 #include "ui/message_center/message_center_style.h"
20 #include "ui/message_center/notification.h"
21 #include "ui/message_center/notification_list.h"
22 #include "ui/message_center/notification_types.h"
23 #include "ui/message_center/views/constants.h"
24 #include "ui/message_center/views/message_center_controller.h"
25 #include "ui/message_center/views/notification_button.h"
26 #include "ui/message_center/views/proportional_image_view.h"
27 #include "ui/views/layout/fill_layout.h"
28 #include "ui/views/test/views_test_base.h"
29 #include "ui/views/test/widget_test.h"
30 #include "ui/views/widget/widget_delegate.h"
32 namespace message_center
{
34 /* Test fixture ***************************************************************/
36 class NotificationViewTest
: public views::ViewsTestBase
,
37 public MessageCenterController
{
39 NotificationViewTest();
40 ~NotificationViewTest() override
;
42 void SetUp() override
;
43 void TearDown() override
;
45 views::Widget
* widget() { return notification_view_
->GetWidget(); }
46 NotificationView
* notification_view() { return notification_view_
.get(); }
47 Notification
* notification() { return notification_
.get(); }
48 RichNotificationData
* data() { return data_
.get(); }
50 // Overridden from MessageCenterController:
51 void ClickOnNotification(const std::string
& notification_id
) override
;
52 void RemoveNotification(const std::string
& notification_id
,
53 bool by_user
) override
;
54 scoped_ptr
<ui::MenuModel
> CreateMenuModel(
55 const NotifierId
& notifier_id
,
56 const base::string16
& display_source
) override
;
57 bool HasClickedListener(const std::string
& notification_id
) override
;
58 void ClickOnNotificationButton(const std::string
& notification_id
,
59 int button_index
) override
;
62 // Used to fill bitmaps returned by CreateBitmap().
63 static const SkColor kBitmapColor
= SK_ColorGREEN
;
65 const gfx::Image
CreateTestImage(int width
, int height
) {
66 return gfx::Image::CreateFrom1xBitmap(CreateBitmap(width
, height
));
69 const SkBitmap
CreateBitmap(int width
, int height
) {
71 bitmap
.allocN32Pixels(width
, height
);
72 bitmap
.eraseColor(kBitmapColor
);
76 std::vector
<ButtonInfo
> CreateButtons(int number
) {
77 ButtonInfo
info(base::ASCIIToUTF16("Test button title."));
78 info
.icon
= CreateTestImage(4, 4);
79 return std::vector
<ButtonInfo
>(number
, info
);
82 // Paints |view| and returns the size that the original image (which must have
83 // been created by CreateBitmap()) was scaled to.
84 gfx::Size
GetImagePaintSize(ProportionalImageView
* view
) {
86 if (view
->bounds().IsEmpty())
89 gfx::Size canvas_size
= view
->bounds().size();
90 gfx::Canvas
canvas(canvas_size
, 1.0 /* image_scale */,
91 true /* is_opaque */);
92 COMPILE_ASSERT(kBitmapColor
!= SK_ColorBLACK
,
93 bitmap_color_matches_background_color
);
94 canvas
.DrawColor(SK_ColorBLACK
);
95 view
->OnPaint(&canvas
);
98 bitmap
.allocN32Pixels(canvas_size
.width(), canvas_size
.height());
99 canvas
.sk_canvas()->readPixels(&bitmap
, 0, 0);
101 // Incrementally inset each edge at its midpoint to find the bounds of the
102 // rect containing the image's color. This assumes that the image is
103 // centered in the canvas.
104 const int kHalfWidth
= canvas_size
.width() / 2;
105 const int kHalfHeight
= canvas_size
.height() / 2;
106 gfx::Rect
rect(canvas_size
);
107 while (rect
.width() > 0 &&
108 bitmap
.getColor(rect
.x(), kHalfHeight
) != kBitmapColor
)
109 rect
.Inset(1, 0, 0, 0);
110 while (rect
.height() > 0 &&
111 bitmap
.getColor(kHalfWidth
, rect
.y()) != kBitmapColor
)
112 rect
.Inset(0, 1, 0, 0);
113 while (rect
.width() > 0 &&
114 bitmap
.getColor(rect
.right() - 1, kHalfHeight
) != kBitmapColor
)
115 rect
.Inset(0, 0, 1, 0);
116 while (rect
.height() > 0 &&
117 bitmap
.getColor(kHalfWidth
, rect
.bottom() - 1) != kBitmapColor
)
118 rect
.Inset(0, 0, 0, 1);
123 void CheckVerticalOrderInNotification() {
124 std::vector
<views::View
*> vertical_order
;
125 vertical_order
.push_back(notification_view()->top_view_
);
126 vertical_order
.push_back(notification_view()->image_view_
);
127 std::copy(notification_view()->action_buttons_
.begin(),
128 notification_view()->action_buttons_
.end(),
129 std::back_inserter(vertical_order
));
130 std::vector
<views::View
*>::iterator current
= vertical_order
.begin();
131 std::vector
<views::View
*>::iterator last
= current
++;
132 while (current
!= vertical_order
.end()) {
133 gfx::Point last_point
= (*last
)->bounds().origin();
134 views::View::ConvertPointToTarget(
135 (*last
), notification_view(), &last_point
);
137 gfx::Point current_point
= (*current
)->bounds().origin();
138 views::View::ConvertPointToTarget(
139 (*current
), notification_view(), ¤t_point
);
141 EXPECT_LT(last_point
.y(), current_point
.y());
146 void UpdateNotificationViews() {
147 notification_view()->CreateOrUpdateViews(*notification());
148 notification_view()->Layout();
152 scoped_ptr
<RichNotificationData
> data_
;
153 scoped_ptr
<Notification
> notification_
;
154 scoped_ptr
<NotificationView
> notification_view_
;
156 DISALLOW_COPY_AND_ASSIGN(NotificationViewTest
);
159 NotificationViewTest::NotificationViewTest() {
162 NotificationViewTest::~NotificationViewTest() {
165 void NotificationViewTest::SetUp() {
166 views::ViewsTestBase::SetUp();
167 // Create a dummy notification.
169 data_
.reset(new RichNotificationData());
170 notification_
.reset(new Notification(
171 NOTIFICATION_TYPE_BASE_FORMAT
, std::string("notification id"),
172 base::UTF8ToUTF16("title"), base::UTF8ToUTF16("message"),
173 CreateTestImage(80, 80), base::UTF8ToUTF16("display source"), GURL(),
174 NotifierId(NotifierId::APPLICATION
, "extension_id"), *data_
, NULL
));
175 notification_
->set_small_image(CreateTestImage(16, 16));
176 notification_
->set_image(CreateTestImage(320, 240));
178 // Then create a new NotificationView with that single notification.
179 notification_view_
.reset(
180 NotificationView::Create(this, *notification_
, true));
181 notification_view_
->set_owned_by_client();
183 views::Widget::InitParams
init_params(
184 CreateParams(views::Widget::InitParams::TYPE_POPUP
));
185 views::Widget
* widget
= new views::Widget();
186 widget
->Init(init_params
);
187 widget
->SetContentsView(notification_view_
.get());
188 widget
->SetSize(notification_view_
->GetPreferredSize());
191 void NotificationViewTest::TearDown() {
193 notification_view_
.reset();
194 views::ViewsTestBase::TearDown();
197 void NotificationViewTest::ClickOnNotification(
198 const std::string
& notification_id
) {
199 // For this test, this method should not be invoked.
203 void NotificationViewTest::RemoveNotification(
204 const std::string
& notification_id
,
206 // For this test, this method should not be invoked.
210 scoped_ptr
<ui::MenuModel
> NotificationViewTest::CreateMenuModel(
211 const NotifierId
& notifier_id
,
212 const base::string16
& display_source
) {
213 // For this test, this method should not be invoked.
218 bool NotificationViewTest::HasClickedListener(
219 const std::string
& notification_id
) {
223 void NotificationViewTest::ClickOnNotificationButton(
224 const std::string
& notification_id
,
226 // For this test, this method should not be invoked.
230 /* Unit tests *****************************************************************/
232 TEST_F(NotificationViewTest
, CreateOrUpdateTest
) {
233 EXPECT_TRUE(NULL
!= notification_view()->title_view_
);
234 EXPECT_TRUE(NULL
!= notification_view()->message_view_
);
235 EXPECT_TRUE(NULL
!= notification_view()->icon_view_
);
236 EXPECT_TRUE(NULL
!= notification_view()->image_view_
);
238 notification()->set_image(gfx::Image());
239 notification()->set_title(base::ASCIIToUTF16(""));
240 notification()->set_message(base::ASCIIToUTF16(""));
241 notification()->set_icon(gfx::Image());
243 notification_view()->CreateOrUpdateViews(*notification());
244 EXPECT_TRUE(NULL
== notification_view()->title_view_
);
245 EXPECT_TRUE(NULL
== notification_view()->message_view_
);
246 EXPECT_TRUE(NULL
== notification_view()->image_view_
);
247 // We still expect an icon view for all layouts.
248 EXPECT_TRUE(NULL
!= notification_view()->icon_view_
);
251 TEST_F(NotificationViewTest
, TestLineLimits
) {
252 notification()->set_image(CreateTestImage(0, 0));
253 notification()->set_context_message(base::ASCIIToUTF16(""));
254 notification_view()->CreateOrUpdateViews(*notification());
256 EXPECT_EQ(5, notification_view()->GetMessageLineLimit(0, 360));
257 EXPECT_EQ(5, notification_view()->GetMessageLineLimit(1, 360));
258 EXPECT_EQ(3, notification_view()->GetMessageLineLimit(2, 360));
260 notification()->set_image(CreateTestImage(2, 2));
261 notification_view()->CreateOrUpdateViews(*notification());
263 EXPECT_EQ(2, notification_view()->GetMessageLineLimit(0, 360));
264 EXPECT_EQ(2, notification_view()->GetMessageLineLimit(1, 360));
265 EXPECT_EQ(1, notification_view()->GetMessageLineLimit(2, 360));
267 notification()->set_context_message(base::ASCIIToUTF16("foo"));
268 notification_view()->CreateOrUpdateViews(*notification());
270 EXPECT_TRUE(notification_view()->context_message_view_
!= NULL
);
272 EXPECT_EQ(1, notification_view()->GetMessageLineLimit(0, 360));
273 EXPECT_EQ(1, notification_view()->GetMessageLineLimit(1, 360));
274 EXPECT_EQ(0, notification_view()->GetMessageLineLimit(2, 360));
277 TEST_F(NotificationViewTest
, TestIconSizing
) {
278 notification()->set_type(NOTIFICATION_TYPE_SIMPLE
);
279 ProportionalImageView
* view
= notification_view()->icon_view_
;
281 // Icons smaller than the legacy size should be scaled up to it.
282 notification()->set_icon(CreateTestImage(kLegacyIconSize
/ 2,
283 kLegacyIconSize
/ 2));
284 UpdateNotificationViews();
285 EXPECT_EQ(gfx::Size(kLegacyIconSize
, kLegacyIconSize
).ToString(),
286 GetImagePaintSize(view
).ToString());
288 // Icons at the legacy size should be unscaled.
289 notification()->set_icon(CreateTestImage(kLegacyIconSize
, kLegacyIconSize
));
290 UpdateNotificationViews();
291 EXPECT_EQ(gfx::Size(kLegacyIconSize
, kLegacyIconSize
).ToString(),
292 GetImagePaintSize(view
).ToString());
294 // Icons slightly smaller than the preferred size should be scaled down to the
295 // legacy size to avoid having tiny borders (http://crbug.com/232966).
296 notification()->set_icon(CreateTestImage(kIconSize
- 1, kIconSize
- 1));
297 UpdateNotificationViews();
298 EXPECT_EQ(gfx::Size(kLegacyIconSize
, kLegacyIconSize
).ToString(),
299 GetImagePaintSize(view
).ToString());
301 // Icons at the preferred size or above should be scaled down to the preferred
303 notification()->set_icon(CreateTestImage(kIconSize
, kIconSize
));
304 UpdateNotificationViews();
305 EXPECT_EQ(gfx::Size(kIconSize
, kIconSize
).ToString(),
306 GetImagePaintSize(view
).ToString());
308 notification()->set_icon(CreateTestImage(2 * kIconSize
, 2 * kIconSize
));
309 UpdateNotificationViews();
310 EXPECT_EQ(gfx::Size(kIconSize
, kIconSize
).ToString(),
311 GetImagePaintSize(view
).ToString());
313 // Large, non-square images' aspect ratios should be preserved.
314 notification()->set_icon(CreateTestImage(4 * kIconSize
, 2 * kIconSize
));
315 UpdateNotificationViews();
316 EXPECT_EQ(gfx::Size(kIconSize
, kIconSize
/ 2).ToString(),
317 GetImagePaintSize(view
).ToString());
320 TEST_F(NotificationViewTest
, TestImageSizing
) {
321 ProportionalImageView
* view
= notification_view()->image_view_
;
322 const gfx::Size
kIdealSize(kNotificationPreferredImageWidth
,
323 kNotificationPreferredImageHeight
);
325 // Images should be scaled to the ideal size.
326 notification()->set_image(CreateTestImage(kIdealSize
.width() / 2,
327 kIdealSize
.height() / 2));
328 UpdateNotificationViews();
329 EXPECT_EQ(kIdealSize
.ToString(), GetImagePaintSize(view
).ToString());
331 notification()->set_image(CreateTestImage(kIdealSize
.width(),
332 kIdealSize
.height()));
333 UpdateNotificationViews();
334 EXPECT_EQ(kIdealSize
.ToString(), GetImagePaintSize(view
).ToString());
336 notification()->set_image(CreateTestImage(kIdealSize
.width() * 2,
337 kIdealSize
.height() * 2));
338 UpdateNotificationViews();
339 EXPECT_EQ(kIdealSize
.ToString(), GetImagePaintSize(view
).ToString());
341 // Original aspect ratios should be preserved.
342 gfx::Size
orig_size(kIdealSize
.width() * 2, kIdealSize
.height());
343 notification()->set_image(
344 CreateTestImage(orig_size
.width(), orig_size
.height()));
345 UpdateNotificationViews();
346 gfx::Size paint_size
= GetImagePaintSize(view
);
347 gfx::Size container_size
= kIdealSize
;
348 container_size
.Enlarge(-2 * kNotificationImageBorderSize
,
349 -2 * kNotificationImageBorderSize
);
350 EXPECT_EQ(GetImageSizeForContainerSize(container_size
, orig_size
).ToString(),
351 paint_size
.ToString());
352 ASSERT_GT(paint_size
.height(), 0);
353 EXPECT_EQ(orig_size
.width() / orig_size
.height(),
354 paint_size
.width() / paint_size
.height());
356 orig_size
.SetSize(kIdealSize
.width(), kIdealSize
.height() * 2);
357 notification()->set_image(
358 CreateTestImage(orig_size
.width(), orig_size
.height()));
359 UpdateNotificationViews();
360 paint_size
= GetImagePaintSize(view
);
361 EXPECT_EQ(GetImageSizeForContainerSize(container_size
, orig_size
).ToString(),
362 paint_size
.ToString());
363 ASSERT_GT(paint_size
.height(), 0);
364 EXPECT_EQ(orig_size
.width() / orig_size
.height(),
365 paint_size
.width() / paint_size
.height());
368 TEST_F(NotificationViewTest
, UpdateButtonsStateTest
) {
369 notification()->set_buttons(CreateButtons(2));
370 notification_view()->CreateOrUpdateViews(*notification());
373 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
374 notification_view()->action_buttons_
[0]->state());
376 // Now construct a mouse move event 1 pixel inside the boundary of the action
378 gfx::Point
cursor_location(1, 1);
379 views::View::ConvertPointToWidget(notification_view()->action_buttons_
[0],
381 ui::MouseEvent
move(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
382 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
383 widget()->OnMouseEvent(&move
);
385 EXPECT_EQ(views::CustomButton::STATE_HOVERED
,
386 notification_view()->action_buttons_
[0]->state());
388 notification_view()->CreateOrUpdateViews(*notification());
390 EXPECT_EQ(views::CustomButton::STATE_HOVERED
,
391 notification_view()->action_buttons_
[0]->state());
393 // Now construct a mouse move event 1 pixel outside the boundary of the
395 cursor_location
= gfx::Point(-1, -1);
396 move
= ui::MouseEvent(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
397 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
398 widget()->OnMouseEvent(&move
);
400 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
401 notification_view()->action_buttons_
[0]->state());
404 TEST_F(NotificationViewTest
, UpdateButtonCountTest
) {
405 notification()->set_buttons(CreateButtons(2));
406 notification_view()->CreateOrUpdateViews(*notification());
409 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
410 notification_view()->action_buttons_
[0]->state());
411 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
412 notification_view()->action_buttons_
[1]->state());
414 // Now construct a mouse move event 1 pixel inside the boundary of the action
416 gfx::Point
cursor_location(1, 1);
417 views::View::ConvertPointToScreen(notification_view()->action_buttons_
[0],
419 ui::MouseEvent
move(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
420 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
421 ui::EventDispatchDetails details
=
422 views::test::WidgetTest::GetEventProcessor(widget())->
423 OnEventFromSource(&move
);
424 EXPECT_FALSE(details
.dispatcher_destroyed
);
426 EXPECT_EQ(views::CustomButton::STATE_HOVERED
,
427 notification_view()->action_buttons_
[0]->state());
428 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
429 notification_view()->action_buttons_
[1]->state());
431 notification()->set_buttons(CreateButtons(1));
432 notification_view()->CreateOrUpdateViews(*notification());
434 EXPECT_EQ(views::CustomButton::STATE_HOVERED
,
435 notification_view()->action_buttons_
[0]->state());
436 EXPECT_EQ(1u, notification_view()->action_buttons_
.size());
438 // Now construct a mouse move event 1 pixel outside the boundary of the
440 cursor_location
= gfx::Point(-1, -1);
441 move
= ui::MouseEvent(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
442 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
443 widget()->OnMouseEvent(&move
);
445 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
446 notification_view()->action_buttons_
[0]->state());
449 TEST_F(NotificationViewTest
, ViewOrderingTest
) {
450 // Tests that views are created in the correct vertical order.
451 notification()->set_buttons(CreateButtons(2));
453 // Layout the initial views.
454 UpdateNotificationViews();
456 // Double-check that vertical order is correct.
457 CheckVerticalOrderInNotification();
459 // Tests that views remain in that order even after an update.
460 UpdateNotificationViews();
461 CheckVerticalOrderInNotification();
464 TEST_F(NotificationViewTest
, FormatContextMessageTest
) {
465 const std::string kRegularContextText
= "Context Text";
466 const std::string kVeryLongContextText
=
467 "VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY"
468 "VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY"
469 "VERY VERY VERY VERY Long Long Long Long Long Long Long Long context";
471 const std::string kVeryLongElidedContextText
=
472 "VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY VERY VERYVERY VERY "
473 "VERY VERY VERY VERY VERY VERY VERY VERY VERY\xE2\x80\xA6";
475 const std::string kChromeUrl
= "chrome://settings";
476 const std::string kUrlContext
= "http://chromium.org/hello";
477 const std::string kHostContext
= "chromium.org";
478 const std::string kLongUrlContext
=
480 "veryveryveryveryveyryveryveryveryveryveyryveryvery.veryveryveyrylong."
481 "chromium.org/hello";
483 Notification
notification1(
484 NOTIFICATION_TYPE_BASE_FORMAT
, std::string(""), base::UTF8ToUTF16(""),
485 base::UTF8ToUTF16(""), CreateTestImage(80, 80), base::UTF8ToUTF16(""),
486 GURL(), message_center::NotifierId(GURL()), *data(), NULL
);
487 notification1
.set_context_message(base::ASCIIToUTF16(kRegularContextText
));
489 base::string16 result
=
490 notification_view()->FormatContextMessage(notification1
);
491 EXPECT_EQ(kRegularContextText
, base::UTF16ToUTF8(result
));
493 notification1
.set_context_message(base::ASCIIToUTF16(kVeryLongContextText
));
494 result
= notification_view()->FormatContextMessage(notification1
);
495 EXPECT_EQ(kVeryLongElidedContextText
, base::UTF16ToUTF8(result
));
497 Notification
notification2(
498 NOTIFICATION_TYPE_BASE_FORMAT
, std::string(""), base::UTF8ToUTF16(""),
499 base::UTF8ToUTF16(""), CreateTestImage(80, 80), base::UTF8ToUTF16(""),
500 GURL(kUrlContext
), message_center::NotifierId(GURL()), *data(), NULL
);
501 notification2
.set_context_message(base::ASCIIToUTF16(""));
503 result
= notification_view()->FormatContextMessage(notification2
);
504 EXPECT_EQ(kHostContext
, base::UTF16ToUTF8(result
));
506 // Non http url and empty context message should yield an empty context
508 Notification
notification3(
509 NOTIFICATION_TYPE_BASE_FORMAT
, std::string(""), base::UTF8ToUTF16(""),
510 base::UTF8ToUTF16(""), CreateTestImage(80, 80), base::UTF8ToUTF16(""),
511 GURL(kChromeUrl
), message_center::NotifierId(GURL()), *data(), NULL
);
512 notification3
.set_context_message(base::ASCIIToUTF16(""));
513 result
= notification_view()->FormatContextMessage(notification3
);
514 EXPECT_TRUE(result
.empty());
516 // Long http url should be elided
517 Notification
notification4(
518 NOTIFICATION_TYPE_BASE_FORMAT
, std::string(""), base::UTF8ToUTF16(""),
519 base::UTF8ToUTF16(""), CreateTestImage(80, 80), base::UTF8ToUTF16(""),
520 GURL(kLongUrlContext
), message_center::NotifierId(GURL()), *data(), NULL
);
521 notification4
.set_context_message(base::ASCIIToUTF16(""));
522 result
= notification_view()->FormatContextMessage(notification4
);
524 // Different platforms elide at different lengths so we do
525 // some generic checking here.
526 // The url has been elided (it starts with an ellipsis)
527 // The end of the domainsuffix is shown
528 // the url piece is not shown
529 EXPECT_TRUE(base::UTF16ToUTF8(result
).find(
530 ".veryveryveyrylong.chromium.org") != std::string::npos
);
531 EXPECT_TRUE(base::UTF16ToUTF8(result
).find("\xE2\x80\xA6") == 0);
532 EXPECT_TRUE(base::UTF16ToUTF8(result
).find("hello") == std::string::npos
);
535 } // namespace message_center