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_utils.h"
14 #include "ui/gfx/canvas.h"
15 #include "ui/gfx/geometry/rect.h"
16 #include "ui/gfx/geometry/size.h"
17 #include "ui/gfx/image/image.h"
18 #include "ui/message_center/message_center_style.h"
19 #include "ui/message_center/notification.h"
20 #include "ui/message_center/notification_list.h"
21 #include "ui/message_center/notification_types.h"
22 #include "ui/message_center/views/constants.h"
23 #include "ui/message_center/views/message_center_controller.h"
24 #include "ui/message_center/views/notification_button.h"
25 #include "ui/message_center/views/proportional_image_view.h"
26 #include "ui/views/layout/fill_layout.h"
27 #include "ui/views/test/views_test_base.h"
28 #include "ui/views/widget/widget_delegate.h"
30 namespace message_center
{
32 /* Test fixture ***************************************************************/
34 class NotificationViewTest
: public views::ViewsTestBase
,
35 public MessageCenterController
{
37 NotificationViewTest();
38 ~NotificationViewTest() override
;
40 void SetUp() override
;
41 void TearDown() override
;
43 views::Widget
* widget() { return notification_view_
->GetWidget(); }
44 NotificationView
* notification_view() { return notification_view_
.get(); }
45 Notification
* notification() { return notification_
.get(); }
46 RichNotificationData
* data() { return data_
.get(); }
48 // Overridden from MessageCenterController:
49 void ClickOnNotification(const std::string
& notification_id
) override
;
50 void RemoveNotification(const std::string
& notification_id
,
51 bool by_user
) override
;
52 scoped_ptr
<ui::MenuModel
> CreateMenuModel(
53 const NotifierId
& notifier_id
,
54 const base::string16
& display_source
) override
;
55 bool HasClickedListener(const std::string
& notification_id
) override
;
56 void ClickOnNotificationButton(const std::string
& notification_id
,
57 int button_index
) override
;
60 // Used to fill bitmaps returned by CreateBitmap().
61 static const SkColor kBitmapColor
= SK_ColorGREEN
;
63 const gfx::Image
CreateTestImage(int width
, int height
) {
64 return gfx::Image::CreateFrom1xBitmap(CreateBitmap(width
, height
));
67 const SkBitmap
CreateBitmap(int width
, int height
) {
69 bitmap
.allocN32Pixels(width
, height
);
70 bitmap
.eraseColor(kBitmapColor
);
74 std::vector
<ButtonInfo
> CreateButtons(int number
) {
75 ButtonInfo
info(base::ASCIIToUTF16("Test button title."));
76 info
.icon
= CreateTestImage(4, 4);
77 return std::vector
<ButtonInfo
>(number
, info
);
80 // Paints |view| and returns the size that the original image (which must have
81 // been created by CreateBitmap()) was scaled to.
82 gfx::Size
GetImagePaintSize(ProportionalImageView
* view
) {
84 if (view
->bounds().IsEmpty())
87 gfx::Size canvas_size
= view
->bounds().size();
88 gfx::Canvas
canvas(canvas_size
, 1.0 /* image_scale */,
89 true /* is_opaque */);
90 COMPILE_ASSERT(kBitmapColor
!= SK_ColorBLACK
,
91 bitmap_color_matches_background_color
);
92 canvas
.DrawColor(SK_ColorBLACK
);
93 view
->OnPaint(&canvas
);
96 bitmap
.allocN32Pixels(canvas_size
.width(), canvas_size
.height());
97 canvas
.sk_canvas()->readPixels(&bitmap
, 0, 0);
99 // Incrementally inset each edge at its midpoint to find the bounds of the
100 // rect containing the image's color. This assumes that the image is
101 // centered in the canvas.
102 const int kHalfWidth
= canvas_size
.width() / 2;
103 const int kHalfHeight
= canvas_size
.height() / 2;
104 gfx::Rect
rect(canvas_size
);
105 while (rect
.width() > 0 &&
106 bitmap
.getColor(rect
.x(), kHalfHeight
) != kBitmapColor
)
107 rect
.Inset(1, 0, 0, 0);
108 while (rect
.height() > 0 &&
109 bitmap
.getColor(kHalfWidth
, rect
.y()) != kBitmapColor
)
110 rect
.Inset(0, 1, 0, 0);
111 while (rect
.width() > 0 &&
112 bitmap
.getColor(rect
.right() - 1, kHalfHeight
) != kBitmapColor
)
113 rect
.Inset(0, 0, 1, 0);
114 while (rect
.height() > 0 &&
115 bitmap
.getColor(kHalfWidth
, rect
.bottom() - 1) != kBitmapColor
)
116 rect
.Inset(0, 0, 0, 1);
121 void CheckVerticalOrderInNotification() {
122 std::vector
<views::View
*> vertical_order
;
123 vertical_order
.push_back(notification_view()->top_view_
);
124 vertical_order
.push_back(notification_view()->image_view_
);
125 std::copy(notification_view()->action_buttons_
.begin(),
126 notification_view()->action_buttons_
.end(),
127 std::back_inserter(vertical_order
));
128 std::vector
<views::View
*>::iterator current
= vertical_order
.begin();
129 std::vector
<views::View
*>::iterator last
= current
++;
130 while (current
!= vertical_order
.end()) {
131 gfx::Point last_point
= (*last
)->bounds().origin();
132 views::View::ConvertPointToTarget(
133 (*last
), notification_view(), &last_point
);
135 gfx::Point current_point
= (*current
)->bounds().origin();
136 views::View::ConvertPointToTarget(
137 (*current
), notification_view(), ¤t_point
);
139 EXPECT_LT(last_point
.y(), current_point
.y());
144 void UpdateNotificationViews() {
145 notification_view()->CreateOrUpdateViews(*notification());
146 notification_view()->Layout();
150 scoped_ptr
<RichNotificationData
> data_
;
151 scoped_ptr
<Notification
> notification_
;
152 scoped_ptr
<NotificationView
> notification_view_
;
154 DISALLOW_COPY_AND_ASSIGN(NotificationViewTest
);
157 NotificationViewTest::NotificationViewTest() {
160 NotificationViewTest::~NotificationViewTest() {
163 void NotificationViewTest::SetUp() {
164 views::ViewsTestBase::SetUp();
165 // Create a dummy notification.
167 data_
.reset(new RichNotificationData());
169 new Notification(NOTIFICATION_TYPE_BASE_FORMAT
,
170 std::string("notification id"),
171 base::UTF8ToUTF16("title"),
172 base::UTF8ToUTF16("message"),
173 CreateTestImage(80, 80),
174 base::UTF8ToUTF16("display source"),
175 NotifierId(NotifierId::APPLICATION
, "extension_id"),
178 notification_
->set_small_image(CreateTestImage(16, 16));
179 notification_
->set_image(CreateTestImage(320, 240));
181 // Then create a new NotificationView with that single notification.
182 notification_view_
.reset(
183 NotificationView::Create(this, *notification_
, true));
184 notification_view_
->set_owned_by_client();
186 views::Widget::InitParams
init_params(
187 CreateParams(views::Widget::InitParams::TYPE_POPUP
));
188 views::Widget
* widget
= new views::Widget();
189 widget
->Init(init_params
);
190 widget
->SetContentsView(notification_view_
.get());
191 widget
->SetSize(notification_view_
->GetPreferredSize());
194 void NotificationViewTest::TearDown() {
196 notification_view_
.reset();
197 views::ViewsTestBase::TearDown();
200 void NotificationViewTest::ClickOnNotification(
201 const std::string
& notification_id
) {
202 // For this test, this method should not be invoked.
206 void NotificationViewTest::RemoveNotification(
207 const std::string
& notification_id
,
209 // For this test, this method should not be invoked.
213 scoped_ptr
<ui::MenuModel
> NotificationViewTest::CreateMenuModel(
214 const NotifierId
& notifier_id
,
215 const base::string16
& display_source
) {
216 // For this test, this method should not be invoked.
221 bool NotificationViewTest::HasClickedListener(
222 const std::string
& notification_id
) {
226 void NotificationViewTest::ClickOnNotificationButton(
227 const std::string
& notification_id
,
229 // For this test, this method should not be invoked.
233 /* Unit tests *****************************************************************/
235 TEST_F(NotificationViewTest
, CreateOrUpdateTest
) {
236 EXPECT_TRUE(NULL
!= notification_view()->title_view_
);
237 EXPECT_TRUE(NULL
!= notification_view()->message_view_
);
238 EXPECT_TRUE(NULL
!= notification_view()->icon_view_
);
239 EXPECT_TRUE(NULL
!= notification_view()->image_view_
);
241 notification()->set_image(gfx::Image());
242 notification()->set_title(base::ASCIIToUTF16(""));
243 notification()->set_message(base::ASCIIToUTF16(""));
244 notification()->set_icon(gfx::Image());
246 notification_view()->CreateOrUpdateViews(*notification());
247 EXPECT_TRUE(NULL
== notification_view()->title_view_
);
248 EXPECT_TRUE(NULL
== notification_view()->message_view_
);
249 EXPECT_TRUE(NULL
== notification_view()->image_view_
);
250 // We still expect an icon view for all layouts.
251 EXPECT_TRUE(NULL
!= notification_view()->icon_view_
);
254 TEST_F(NotificationViewTest
, TestLineLimits
) {
255 notification()->set_image(CreateTestImage(0, 0));
256 notification()->set_context_message(base::ASCIIToUTF16(""));
257 notification_view()->CreateOrUpdateViews(*notification());
259 EXPECT_EQ(5, notification_view()->GetMessageLineLimit(0, 360));
260 EXPECT_EQ(5, notification_view()->GetMessageLineLimit(1, 360));
261 EXPECT_EQ(3, notification_view()->GetMessageLineLimit(2, 360));
263 notification()->set_image(CreateTestImage(2, 2));
264 notification_view()->CreateOrUpdateViews(*notification());
266 EXPECT_EQ(2, notification_view()->GetMessageLineLimit(0, 360));
267 EXPECT_EQ(2, notification_view()->GetMessageLineLimit(1, 360));
268 EXPECT_EQ(1, notification_view()->GetMessageLineLimit(2, 360));
270 notification()->set_context_message(base::UTF8ToUTF16("foo"));
271 notification_view()->CreateOrUpdateViews(*notification());
273 EXPECT_TRUE(notification_view()->context_message_view_
!= NULL
);
275 EXPECT_EQ(1, notification_view()->GetMessageLineLimit(0, 360));
276 EXPECT_EQ(1, notification_view()->GetMessageLineLimit(1, 360));
277 EXPECT_EQ(0, notification_view()->GetMessageLineLimit(2, 360));
280 TEST_F(NotificationViewTest
, TestIconSizing
) {
281 notification()->set_type(NOTIFICATION_TYPE_SIMPLE
);
282 ProportionalImageView
* view
= notification_view()->icon_view_
;
284 // Icons smaller than the legacy size should be scaled up to it.
285 notification()->set_icon(CreateTestImage(kLegacyIconSize
/ 2,
286 kLegacyIconSize
/ 2));
287 UpdateNotificationViews();
288 EXPECT_EQ(gfx::Size(kLegacyIconSize
, kLegacyIconSize
).ToString(),
289 GetImagePaintSize(view
).ToString());
291 // Icons at the legacy size should be unscaled.
292 notification()->set_icon(CreateTestImage(kLegacyIconSize
, kLegacyIconSize
));
293 UpdateNotificationViews();
294 EXPECT_EQ(gfx::Size(kLegacyIconSize
, kLegacyIconSize
).ToString(),
295 GetImagePaintSize(view
).ToString());
297 // Icons slightly smaller than the preferred size should be scaled down to the
298 // legacy size to avoid having tiny borders (http://crbug.com/232966).
299 notification()->set_icon(CreateTestImage(kIconSize
- 1, kIconSize
- 1));
300 UpdateNotificationViews();
301 EXPECT_EQ(gfx::Size(kLegacyIconSize
, kLegacyIconSize
).ToString(),
302 GetImagePaintSize(view
).ToString());
304 // Icons at the preferred size or above should be scaled down to the preferred
306 notification()->set_icon(CreateTestImage(kIconSize
, kIconSize
));
307 UpdateNotificationViews();
308 EXPECT_EQ(gfx::Size(kIconSize
, kIconSize
).ToString(),
309 GetImagePaintSize(view
).ToString());
311 notification()->set_icon(CreateTestImage(2 * kIconSize
, 2 * kIconSize
));
312 UpdateNotificationViews();
313 EXPECT_EQ(gfx::Size(kIconSize
, kIconSize
).ToString(),
314 GetImagePaintSize(view
).ToString());
316 // Large, non-square images' aspect ratios should be preserved.
317 notification()->set_icon(CreateTestImage(4 * kIconSize
, 2 * kIconSize
));
318 UpdateNotificationViews();
319 EXPECT_EQ(gfx::Size(kIconSize
, kIconSize
/ 2).ToString(),
320 GetImagePaintSize(view
).ToString());
323 TEST_F(NotificationViewTest
, TestImageSizing
) {
324 ProportionalImageView
* view
= notification_view()->image_view_
;
325 const gfx::Size
kIdealSize(kNotificationPreferredImageWidth
,
326 kNotificationPreferredImageHeight
);
328 // Images should be scaled to the ideal size.
329 notification()->set_image(CreateTestImage(kIdealSize
.width() / 2,
330 kIdealSize
.height() / 2));
331 UpdateNotificationViews();
332 EXPECT_EQ(kIdealSize
.ToString(), GetImagePaintSize(view
).ToString());
334 notification()->set_image(CreateTestImage(kIdealSize
.width(),
335 kIdealSize
.height()));
336 UpdateNotificationViews();
337 EXPECT_EQ(kIdealSize
.ToString(), GetImagePaintSize(view
).ToString());
339 notification()->set_image(CreateTestImage(kIdealSize
.width() * 2,
340 kIdealSize
.height() * 2));
341 UpdateNotificationViews();
342 EXPECT_EQ(kIdealSize
.ToString(), GetImagePaintSize(view
).ToString());
344 // Original aspect ratios should be preserved.
345 gfx::Size
orig_size(kIdealSize
.width() * 2, kIdealSize
.height());
346 notification()->set_image(
347 CreateTestImage(orig_size
.width(), orig_size
.height()));
348 UpdateNotificationViews();
349 gfx::Size paint_size
= GetImagePaintSize(view
);
350 gfx::Size container_size
= kIdealSize
;
351 container_size
.Enlarge(-2 * kNotificationImageBorderSize
,
352 -2 * kNotificationImageBorderSize
);
353 EXPECT_EQ(GetImageSizeForContainerSize(container_size
, orig_size
).ToString(),
354 paint_size
.ToString());
355 ASSERT_GT(paint_size
.height(), 0);
356 EXPECT_EQ(orig_size
.width() / orig_size
.height(),
357 paint_size
.width() / paint_size
.height());
359 orig_size
.SetSize(kIdealSize
.width(), kIdealSize
.height() * 2);
360 notification()->set_image(
361 CreateTestImage(orig_size
.width(), orig_size
.height()));
362 UpdateNotificationViews();
363 paint_size
= GetImagePaintSize(view
);
364 EXPECT_EQ(GetImageSizeForContainerSize(container_size
, orig_size
).ToString(),
365 paint_size
.ToString());
366 ASSERT_GT(paint_size
.height(), 0);
367 EXPECT_EQ(orig_size
.width() / orig_size
.height(),
368 paint_size
.width() / paint_size
.height());
371 TEST_F(NotificationViewTest
, UpdateButtonsStateTest
) {
372 notification()->set_buttons(CreateButtons(2));
373 notification_view()->CreateOrUpdateViews(*notification());
376 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
377 notification_view()->action_buttons_
[0]->state());
379 // Now construct a mouse move event 1 pixel inside the boundary of the action
381 gfx::Point
cursor_location(1, 1);
382 views::View::ConvertPointToWidget(notification_view()->action_buttons_
[0],
384 ui::MouseEvent
move(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
385 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
386 widget()->OnMouseEvent(&move
);
388 EXPECT_EQ(views::CustomButton::STATE_HOVERED
,
389 notification_view()->action_buttons_
[0]->state());
391 notification_view()->CreateOrUpdateViews(*notification());
393 EXPECT_EQ(views::CustomButton::STATE_HOVERED
,
394 notification_view()->action_buttons_
[0]->state());
396 // Now construct a mouse move event 1 pixel outside the boundary of the
398 cursor_location
= gfx::Point(-1, -1);
399 move
= ui::MouseEvent(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
400 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
401 widget()->OnMouseEvent(&move
);
403 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
404 notification_view()->action_buttons_
[0]->state());
407 TEST_F(NotificationViewTest
, UpdateButtonCountTest
) {
408 notification()->set_buttons(CreateButtons(2));
409 notification_view()->CreateOrUpdateViews(*notification());
412 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
413 notification_view()->action_buttons_
[0]->state());
414 EXPECT_EQ(views::CustomButton::STATE_NORMAL
,
415 notification_view()->action_buttons_
[1]->state());
417 // Now construct a mouse move event 1 pixel inside the boundary of the action
419 gfx::Point
cursor_location(1, 1);
420 views::View::ConvertPointToWidget(notification_view()->action_buttons_
[0],
422 ui::MouseEvent
move(ui::ET_MOUSE_MOVED
, cursor_location
, cursor_location
,
423 ui::EventTimeForNow(), ui::EF_NONE
, ui::EF_NONE
);
424 widget()->OnMouseEvent(&move
);
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 } // namespace message_center