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.
5 #include "ui/app_list/views/apps_grid_view.h"
9 #include "base/basictypes.h"
10 #include "base/compiler_specific.h"
11 #include "base/memory/scoped_ptr.h"
12 #include "base/message_loop/message_loop.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/timer/timer.h"
15 #include "testing/gtest/include/gtest/gtest.h"
16 #include "ui/app_list/app_list_item_model.h"
17 #include "ui/app_list/app_list_model.h"
18 #include "ui/app_list/pagination_model.h"
19 #include "ui/app_list/test/app_list_test_model.h"
20 #include "ui/app_list/views/app_list_item_view.h"
21 #include "ui/app_list/views/test/apps_grid_view_test_api.h"
22 #include "ui/views/test/views_test_base.h"
29 const int kIconDimension
= 48;
32 const int kTilesPerPage
= kCols
* kRows
;
34 const int kWidth
= 320;
35 const int kHeight
= 240;
37 class PageFlipWaiter
: public PaginationModelObserver
{
39 PageFlipWaiter(base::MessageLoopForUI
* ui_loop
, PaginationModel
* model
)
40 : ui_loop_(ui_loop
), model_(model
), wait_(false), page_changed_(false) {
41 model_
->AddObserver(this);
44 virtual ~PageFlipWaiter() {
45 model_
->RemoveObserver(this);
48 bool Wait(int time_out_ms
) {
51 page_changed_
= false;
55 wait_timer_
.Start(FROM_HERE
,
56 base::TimeDelta::FromMilliseconds(time_out_ms
),
57 this, &PageFlipWaiter::OnWaitTimeOut
);
66 void OnWaitTimeOut() {
70 // PaginationModelObserver overrides:
71 virtual void TotalPagesChanged() OVERRIDE
{
73 virtual void SelectedPageChanged(int old_selected
,
74 int new_selected
) OVERRIDE
{
79 virtual void TransitionStarted() OVERRIDE
{
81 virtual void TransitionChanged() OVERRIDE
{
84 base::MessageLoopForUI
* ui_loop_
;
85 PaginationModel
* model_
;
88 base::OneShotTimer
<PageFlipWaiter
> wait_timer_
;
90 DISALLOW_COPY_AND_ASSIGN(PageFlipWaiter
);
95 class AppsGridViewTest
: public views::ViewsTestBase
{
98 virtual ~AppsGridViewTest() {}
100 // testing::Test overrides:
101 virtual void SetUp() OVERRIDE
{
102 views::ViewsTestBase::SetUp();
103 model_
.reset(new AppListTestModel
);
104 pagination_model_
.reset(new PaginationModel
);
106 apps_grid_view_
.reset(
107 new AppsGridView(NULL
, pagination_model_
.get(), NULL
));
108 apps_grid_view_
->SetLayout(kIconDimension
, kCols
, kRows
);
109 apps_grid_view_
->SetBoundsRect(gfx::Rect(gfx::Size(kWidth
, kHeight
)));
110 apps_grid_view_
->SetModel(model_
.get());
111 apps_grid_view_
->SetItemList(model_
->item_list());
113 test_api_
.reset(new AppsGridViewTestApi(apps_grid_view_
.get()));
115 virtual void TearDown() OVERRIDE
{
116 apps_grid_view_
.reset(); // Release apps grid view before models.
117 views::ViewsTestBase::TearDown();
121 AppListItemView
* GetItemViewAt(int index
) {
122 return static_cast<AppListItemView
*>(
123 test_api_
->GetViewAtModelIndex(index
));
126 AppListItemView
* GetItemViewForPoint(const gfx::Point
& point
) {
127 for (size_t i
= 0; i
< model_
->item_list()->item_count(); ++i
) {
128 AppListItemView
* view
= GetItemViewAt(i
);
129 if (view
->bounds().Contains(point
))
135 gfx::Rect
GetItemTileRectAt(int row
, int col
) {
136 DCHECK_GT(model_
->item_list()->item_count(), 0u);
138 gfx::Insets
insets(apps_grid_view_
->GetInsets());
139 gfx::Rect
rect(gfx::Point(insets
.left(), insets
.top()),
140 GetItemViewAt(0)->bounds().size());
141 rect
.Offset(col
* rect
.width(), row
* rect
.height());
145 // Points are in |apps_grid_view_|'s coordinates.
146 void SimulateDrag(AppsGridView::Pointer pointer
,
147 const gfx::Point
& from
,
148 const gfx::Point
& to
) {
149 AppListItemView
* view
= GetItemViewForPoint(from
);
152 gfx::Point translated_from
= gfx::PointAtOffsetFromOrigin(
153 from
- view
->bounds().origin());
154 gfx::Point translated_to
= gfx::PointAtOffsetFromOrigin(
155 to
- view
->bounds().origin());
157 ui::MouseEvent
pressed_event(ui::ET_MOUSE_PRESSED
,
158 translated_from
, from
, 0, 0);
159 apps_grid_view_
->InitiateDrag(view
, pointer
, pressed_event
);
161 ui::MouseEvent
drag_event(ui::ET_MOUSE_DRAGGED
,
162 translated_to
, to
, 0, 0);
163 apps_grid_view_
->UpdateDragFromItem(pointer
, drag_event
);
166 void SimulateKeyPress(ui::KeyboardCode key_code
) {
167 ui::KeyEvent
key_event(ui::ET_KEY_PRESSED
, key_code
, 0, false);
168 apps_grid_view_
->OnKeyPressed(key_event
);
171 scoped_ptr
<AppListTestModel
> model_
;
172 scoped_ptr
<PaginationModel
> pagination_model_
;
173 scoped_ptr
<AppsGridView
> apps_grid_view_
;
174 scoped_ptr
<AppsGridViewTestApi
> test_api_
;
177 DISALLOW_COPY_AND_ASSIGN(AppsGridViewTest
);
180 TEST_F(AppsGridViewTest
, CreatePage
) {
181 // Fully populates a page.
182 const int kPages
= 1;
183 model_
->PopulateApps(kPages
* kTilesPerPage
);
184 EXPECT_EQ(kPages
, pagination_model_
->total_pages());
186 // Adds one more and gets a new page created.
187 model_
->CreateAndAddItem("Extra");
188 EXPECT_EQ(kPages
+ 1, pagination_model_
->total_pages());
191 TEST_F(AppsGridViewTest
, EnsureHighlightedVisible
) {
192 const int kPages
= 3;
193 model_
->PopulateApps(kPages
* kTilesPerPage
);
194 EXPECT_EQ(kPages
, pagination_model_
->total_pages());
195 EXPECT_EQ(0, pagination_model_
->selected_page());
197 // Highlight first one and last one one first page and first page should be
199 model_
->HighlightItemAt(0);
200 EXPECT_EQ(0, pagination_model_
->selected_page());
201 model_
->HighlightItemAt(kTilesPerPage
- 1);
202 EXPECT_EQ(0, pagination_model_
->selected_page());
204 // Highlight first one on 2nd page and 2nd page should be selected.
205 model_
->HighlightItemAt(kTilesPerPage
+ 1);
206 EXPECT_EQ(1, pagination_model_
->selected_page());
208 // Highlight last one in the model and last page should be selected.
209 model_
->HighlightItemAt(model_
->item_list()->item_count() - 1);
210 EXPECT_EQ(kPages
- 1, pagination_model_
->selected_page());
213 TEST_F(AppsGridViewTest
, RemoveSelectedLastApp
) {
214 const int kTotalItems
= 2;
215 const int kLastItemIndex
= kTotalItems
- 1;
217 model_
->PopulateApps(kTotalItems
);
219 AppListItemView
* last_view
= GetItemViewAt(kLastItemIndex
);
220 apps_grid_view_
->SetSelectedView(last_view
);
221 model_
->item_list()->DeleteItem(model_
->GetItemName(kLastItemIndex
));
223 EXPECT_FALSE(apps_grid_view_
->IsSelectedView(last_view
));
226 AppListItemView
* view
= GetItemViewAt(0);
227 apps_grid_view_
->SetSelectedView(view
);
228 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(view
));
231 TEST_F(AppsGridViewTest
, MouseDrag
) {
232 const int kTotalItems
= 4;
233 model_
->PopulateApps(kTotalItems
);
234 EXPECT_EQ(std::string("Item 0,Item 1,Item 2,Item 3"),
235 model_
->GetModelContent());
237 gfx::Point from
= GetItemTileRectAt(0, 0).CenterPoint();
238 gfx::Point to
= GetItemTileRectAt(0, 1).CenterPoint();
240 // Dragging changes model order.
241 SimulateDrag(AppsGridView::MOUSE
, from
, to
);
242 apps_grid_view_
->EndDrag(false);
243 EXPECT_EQ(std::string("Item 1,Item 0,Item 2,Item 3"),
244 model_
->GetModelContent());
245 test_api_
->LayoutToIdealBounds();
247 // Canceling drag should keep existing order.
248 SimulateDrag(AppsGridView::MOUSE
, from
, to
);
249 apps_grid_view_
->EndDrag(true);
250 EXPECT_EQ(std::string("Item 1,Item 0,Item 2,Item 3"),
251 model_
->GetModelContent());
252 test_api_
->LayoutToIdealBounds();
254 // Deleting an item keeps remaining intact.
255 SimulateDrag(AppsGridView::MOUSE
, from
, to
);
256 model_
->item_list()->DeleteItem(model_
->GetItemName(0));
257 apps_grid_view_
->EndDrag(false);
258 EXPECT_EQ(std::string("Item 1,Item 2,Item 3"),
259 model_
->GetModelContent());
260 test_api_
->LayoutToIdealBounds();
262 // Adding a launcher item cancels the drag and respects the order.
263 SimulateDrag(AppsGridView::MOUSE
, from
, to
);
264 model_
->CreateAndAddItem("Extra");
265 apps_grid_view_
->EndDrag(false);
266 EXPECT_EQ(std::string("Item 1,Item 2,Item 3,Extra"),
267 model_
->GetModelContent());
268 test_api_
->LayoutToIdealBounds();
271 TEST_F(AppsGridViewTest
, MouseDragFlipPage
) {
272 test_api_
->SetPageFlipDelay(10);
273 pagination_model_
->SetTransitionDurations(10, 10);
275 PageFlipWaiter
page_flip_waiter(message_loop(),
276 pagination_model_
.get());
278 const int kPages
= 3;
279 model_
->PopulateApps(kPages
* kTilesPerPage
);
280 EXPECT_EQ(kPages
, pagination_model_
->total_pages());
281 EXPECT_EQ(0, pagination_model_
->selected_page());
283 gfx::Point from
= GetItemTileRectAt(0, 0).CenterPoint();
284 gfx::Point to
= gfx::Point(apps_grid_view_
->width(),
285 apps_grid_view_
->height() / 2);
287 // Drag to right edge.
288 SimulateDrag(AppsGridView::MOUSE
, from
, to
);
290 // Page should be flipped after sometime.
291 EXPECT_TRUE(page_flip_waiter
.Wait(0));
292 EXPECT_EQ(1, pagination_model_
->selected_page());
294 // Stay there and page should be flipped again.
295 EXPECT_TRUE(page_flip_waiter
.Wait(0));
296 EXPECT_EQ(2, pagination_model_
->selected_page());
298 // Stay there longer and no page flip happen since we are at the last page.
299 EXPECT_FALSE(page_flip_waiter
.Wait(100));
300 EXPECT_EQ(2, pagination_model_
->selected_page());
302 apps_grid_view_
->EndDrag(true);
304 // Now drag to the left edge and test the other direction.
307 SimulateDrag(AppsGridView::MOUSE
, from
, to
);
309 EXPECT_TRUE(page_flip_waiter
.Wait(0));
310 EXPECT_EQ(1, pagination_model_
->selected_page());
312 EXPECT_TRUE(page_flip_waiter
.Wait(0));
313 EXPECT_EQ(0, pagination_model_
->selected_page());
315 EXPECT_FALSE(page_flip_waiter
.Wait(100));
316 EXPECT_EQ(0, pagination_model_
->selected_page());
317 apps_grid_view_
->EndDrag(true);
320 TEST_F(AppsGridViewTest
, SimultaneousDrag
) {
321 const int kTotalItems
= 4;
322 model_
->PopulateApps(kTotalItems
);
323 EXPECT_EQ(std::string("Item 0,Item 1,Item 2,Item 3"),
324 model_
->GetModelContent());
326 gfx::Point mouse_from
= GetItemTileRectAt(0, 0).CenterPoint();
327 gfx::Point mouse_to
= GetItemTileRectAt(0, 1).CenterPoint();
329 gfx::Point touch_from
= GetItemTileRectAt(1, 0).CenterPoint();
330 gfx::Point touch_to
= GetItemTileRectAt(1, 1).CenterPoint();
332 // Starts a mouse drag first then a touch drag.
333 SimulateDrag(AppsGridView::MOUSE
, mouse_from
, mouse_to
);
334 SimulateDrag(AppsGridView::TOUCH
, touch_from
, touch_to
);
335 // Finishes the drag and mouse drag wins.
336 apps_grid_view_
->EndDrag(false);
337 EXPECT_EQ(std::string("Item 1,Item 0,Item 2,Item 3"),
338 model_
->GetModelContent());
339 test_api_
->LayoutToIdealBounds();
341 // Starts a touch drag first then a mouse drag.
342 SimulateDrag(AppsGridView::TOUCH
, touch_from
, touch_to
);
343 SimulateDrag(AppsGridView::MOUSE
, mouse_from
, mouse_to
);
344 // Finishes the drag and touch drag wins.
345 apps_grid_view_
->EndDrag(false);
346 EXPECT_EQ(std::string("Item 1,Item 0,Item 3,Item 2"),
347 model_
->GetModelContent());
348 test_api_
->LayoutToIdealBounds();
351 TEST_F(AppsGridViewTest
, HighlightWithKeyboard
) {
352 const int kPages
= 3;
353 const int kItems
= (kPages
- 1) * kTilesPerPage
+ 1;
354 model_
->PopulateApps(kItems
);
356 const int first_index
= 0;
357 const int last_index
= kItems
- 1;
358 const int last_index_on_page1_first_row
= kRows
- 1;
359 const int last_index_on_page1
= kTilesPerPage
- 1;
360 const int first_index_on_page2
= kTilesPerPage
;
361 const int first_index_on_page2_last_row
= 2 * kTilesPerPage
- kRows
;
362 const int last_index_on_page2_last_row
= 2 * kTilesPerPage
- 1;
364 // Try moving off the item beyond the first one.
365 apps_grid_view_
->SetSelectedView(GetItemViewAt(first_index
));
366 SimulateKeyPress(ui::VKEY_UP
);
367 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(first_index
)));
368 SimulateKeyPress(ui::VKEY_LEFT
);
369 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(first_index
)));
371 // Move to the last item and try to go past it.
372 apps_grid_view_
->SetSelectedView(GetItemViewAt(last_index
));
373 SimulateKeyPress(ui::VKEY_DOWN
);
374 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(last_index
)));
375 SimulateKeyPress(ui::VKEY_RIGHT
);
376 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(last_index
)));
378 // Move right on last item on page 1 should get to first item on page 2's last
379 // row and vice versa.
380 apps_grid_view_
->SetSelectedView(GetItemViewAt(last_index_on_page1
));
381 SimulateKeyPress(ui::VKEY_RIGHT
);
382 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
383 first_index_on_page2_last_row
)));
384 SimulateKeyPress(ui::VKEY_LEFT
);
385 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
386 last_index_on_page1
)));
388 // Up/down on page boundary does nothing.
389 apps_grid_view_
->SetSelectedView(GetItemViewAt(last_index_on_page1
));
390 SimulateKeyPress(ui::VKEY_DOWN
);
391 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
392 last_index_on_page1
)));
393 apps_grid_view_
->SetSelectedView(
394 GetItemViewAt(first_index_on_page2_last_row
));
396 SetSelectedView(GetItemViewAt(last_index_on_page1_first_row
));
397 SimulateKeyPress(ui::VKEY_UP
);
398 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
399 last_index_on_page1_first_row
)));
401 // Page up and down should go to the same item on the next and last page.
402 apps_grid_view_
->SetSelectedView(GetItemViewAt(first_index_on_page2
));
403 SimulateKeyPress(ui::VKEY_PRIOR
);
404 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
406 SimulateKeyPress(ui::VKEY_NEXT
);
407 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
408 first_index_on_page2
)));
410 // Moving onto a a page with too few apps to support the expected index snaps
411 // to the last available index.
412 apps_grid_view_
->SetSelectedView(GetItemViewAt(last_index_on_page2_last_row
));
413 SimulateKeyPress(ui::VKEY_RIGHT
);
414 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
416 apps_grid_view_
->SetSelectedView(GetItemViewAt(last_index_on_page2_last_row
));
417 SimulateKeyPress(ui::VKEY_NEXT
);
418 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
423 // After page switch, arrow keys select first item on current page.
424 apps_grid_view_
->SetSelectedView(GetItemViewAt(first_index
));
425 pagination_model_
->SelectPage(1, false);
426 SimulateKeyPress(ui::VKEY_UP
);
427 EXPECT_TRUE(apps_grid_view_
->IsSelectedView(GetItemViewAt(
428 first_index_on_page2
)));
431 TEST_F(AppsGridViewTest
, ItemLabelShortNameOverride
) {
432 // If the app's full name and short name differ, the title label's tooltip
433 // should always be the full name of the app.
434 std::string
expected_text("xyz");
435 std::string
expected_tooltip("tooltip");
436 model_
->CreateAndAddItem(expected_text
, expected_tooltip
);
438 base::string16 actual_tooltip
;
439 AppListItemView
* item_view
= GetItemViewAt(0);
440 ASSERT_TRUE(item_view
);
441 const views::Label
* title_label
= item_view
->title();
442 EXPECT_TRUE(title_label
->GetTooltipText(
443 title_label
->bounds().CenterPoint(), &actual_tooltip
));
444 EXPECT_EQ(expected_tooltip
, UTF16ToUTF8(actual_tooltip
));
445 EXPECT_EQ(expected_text
, UTF16ToUTF8(title_label
->text()));
448 TEST_F(AppsGridViewTest
, ItemLabelNoShortName
) {
449 // If the app's full name and short name are the same, use the default tooltip
450 // behavior of the label (only show a tooltip if the title is truncated).
451 std::string
title("a");
452 model_
->CreateAndAddItem(title
, title
);
454 base::string16 actual_tooltip
;
455 AppListItemView
* item_view
= GetItemViewAt(0);
456 ASSERT_TRUE(item_view
);
457 const views::Label
* title_label
= item_view
->title();
458 EXPECT_FALSE(title_label
->GetTooltipText(
459 title_label
->bounds().CenterPoint(), &actual_tooltip
));
460 EXPECT_EQ(title
, UTF16ToUTF8(title_label
->text()));
464 } // namespace app_list