1 // Copyright 2013 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/app_list_main_view.h"
7 #include "base/memory/scoped_ptr.h"
8 #include "base/run_loop.h"
9 #include "base/time/time.h"
10 #include "base/timer/timer.h"
11 #include "testing/gtest/include/gtest/gtest.h"
12 #include "ui/app_list/app_list_switches.h"
13 #include "ui/app_list/test/app_list_test_model.h"
14 #include "ui/app_list/test/app_list_test_view_delegate.h"
15 #include "ui/app_list/views/app_list_folder_view.h"
16 #include "ui/app_list/views/app_list_item_view.h"
17 #include "ui/app_list/views/apps_container_view.h"
18 #include "ui/app_list/views/apps_grid_view.h"
19 #include "ui/app_list/views/contents_view.h"
20 #include "ui/app_list/views/search_box_view.h"
21 #include "ui/app_list/views/test/apps_grid_view_test_api.h"
22 #include "ui/events/event_utils.h"
23 #include "ui/events/test/event_generator.h"
24 #include "ui/views/test/views_test_base.h"
25 #include "ui/views/view_model.h"
26 #include "ui/views/widget/widget.h"
33 const int kInitialItems
= 2;
35 class GridViewVisibleWaiter
{
37 explicit GridViewVisibleWaiter(AppsGridView
* grid_view
)
38 : grid_view_(grid_view
) {}
39 ~GridViewVisibleWaiter() {}
42 if (grid_view_
->visible())
45 check_timer_
.Start(FROM_HERE
,
46 base::TimeDelta::FromMilliseconds(50),
47 base::Bind(&GridViewVisibleWaiter::OnTimerCheck
,
48 base::Unretained(this)));
49 run_loop_
.reset(new base::RunLoop
);
56 if (grid_view_
->visible())
60 AppsGridView
* grid_view_
;
61 scoped_ptr
<base::RunLoop
> run_loop_
;
62 base::RepeatingTimer
<GridViewVisibleWaiter
> check_timer_
;
64 DISALLOW_COPY_AND_ASSIGN(GridViewVisibleWaiter
);
67 class AppListMainViewTest
: public views::ViewsTestBase
{
70 : main_widget_(nullptr),
72 search_box_widget_(nullptr),
73 search_box_view_(nullptr) {}
75 ~AppListMainViewTest() override
{}
77 // testing::Test overrides:
78 void SetUp() override
{
79 views::ViewsTestBase::SetUp();
80 delegate_
.reset(new AppListTestViewDelegate
);
82 // In Ash, the third argument is a container aura::Window, but it is always
83 // NULL on Windows, and not needed for tests. It is only used to determine
84 // the scale factor for preloading icons.
85 main_view_
= new AppListMainView(delegate_
.get());
86 main_view_
->SetPaintToLayer(true);
87 main_view_
->model()->SetFoldersEnabled(true);
88 search_box_view_
= new SearchBoxView(main_view_
, delegate_
.get());
89 main_view_
->Init(nullptr, 0, search_box_view_
);
91 main_widget_
= new views::Widget
;
92 views::Widget::InitParams main_widget_params
=
93 CreateParams(views::Widget::InitParams::TYPE_POPUP
);
94 main_widget_params
.bounds
.set_size(main_view_
->GetPreferredSize());
95 main_widget_
->Init(main_widget_params
);
96 main_widget_
->SetContentsView(main_view_
);
98 search_box_widget_
= new views::Widget
;
99 views::Widget::InitParams search_box_widget_params
=
100 CreateParams(views::Widget::InitParams::TYPE_CONTROL
);
101 search_box_widget_params
.parent
= main_widget_
->GetNativeView();
102 search_box_widget_params
.opacity
=
103 views::Widget::InitParams::TRANSLUCENT_WINDOW
;
104 search_box_widget_
->Init(search_box_widget_params
);
105 search_box_widget_
->SetContentsView(search_box_view_
);
108 void TearDown() override
{
109 main_widget_
->Close();
110 views::ViewsTestBase::TearDown();
114 // |point| is in |grid_view|'s coordinates.
115 AppListItemView
* GetItemViewAtPointInGrid(AppsGridView
* grid_view
,
116 const gfx::Point
& point
) {
117 const views::ViewModelT
<AppListItemView
>* view_model
=
118 grid_view
->view_model_for_test();
119 for (int i
= 0; i
< view_model
->view_size(); ++i
) {
120 views::View
* view
= view_model
->view_at(i
);
121 if (view
->bounds().Contains(point
)) {
122 return static_cast<AppListItemView
*>(view
);
129 void SimulateClick(views::View
* view
) {
130 gfx::Point center
= view
->GetLocalBounds().CenterPoint();
131 view
->OnMousePressed(ui::MouseEvent(
132 ui::ET_MOUSE_PRESSED
, center
, center
, ui::EventTimeForNow(),
133 ui::EF_LEFT_MOUSE_BUTTON
, ui::EF_LEFT_MOUSE_BUTTON
));
134 view
->OnMouseReleased(ui::MouseEvent(
135 ui::ET_MOUSE_RELEASED
, center
, center
, ui::EventTimeForNow(),
136 ui::EF_LEFT_MOUSE_BUTTON
, ui::EF_LEFT_MOUSE_BUTTON
));
139 // |point| is in |grid_view|'s coordinates.
140 AppListItemView
* SimulateInitiateDrag(AppsGridView
* grid_view
,
141 AppsGridView::Pointer pointer
,
142 const gfx::Point
& point
) {
143 AppListItemView
* view
= GetItemViewAtPointInGrid(grid_view
, point
);
146 gfx::Point translated
=
147 gfx::PointAtOffsetFromOrigin(point
- view
->bounds().origin());
148 ui::MouseEvent
pressed_event(ui::ET_MOUSE_PRESSED
, translated
, point
,
149 ui::EventTimeForNow(), 0, 0);
150 grid_view
->InitiateDrag(view
, pointer
, pressed_event
);
154 // |point| is in |grid_view|'s coordinates.
155 void SimulateUpdateDrag(AppsGridView
* grid_view
,
156 AppsGridView::Pointer pointer
,
157 AppListItemView
* drag_view
,
158 const gfx::Point
& point
) {
160 gfx::Point translated
=
161 gfx::PointAtOffsetFromOrigin(point
- drag_view
->bounds().origin());
162 ui::MouseEvent
drag_event(ui::ET_MOUSE_DRAGGED
, translated
, point
,
163 ui::EventTimeForNow(), 0, 0);
164 grid_view
->UpdateDragFromItem(pointer
, drag_event
);
167 ContentsView
* GetContentsView() { return main_view_
->contents_view(); }
169 AppsGridView
* RootGridView() {
170 return GetContentsView()->apps_container_view()->apps_grid_view();
173 AppListFolderView
* FolderView() {
174 return GetContentsView()->apps_container_view()->app_list_folder_view();
177 AppsGridView
* FolderGridView() { return FolderView()->items_grid_view(); }
179 const views::ViewModelT
<AppListItemView
>* RootViewModel() {
180 return RootGridView()->view_model_for_test();
183 const views::ViewModelT
<AppListItemView
>* FolderViewModel() {
184 return FolderGridView()->view_model_for_test();
187 AppListItemView
* CreateAndOpenSingleItemFolder() {
188 // Prepare single folder with a single item in it.
189 AppListFolderItem
* folder_item
=
190 delegate_
->GetTestModel()->CreateSingleItemFolder("single_item_folder",
192 EXPECT_EQ(folder_item
,
193 delegate_
->GetTestModel()->FindFolderItem("single_item_folder"));
194 EXPECT_EQ(AppListFolderItem::kItemType
, folder_item
->GetItemType());
196 EXPECT_EQ(1, RootViewModel()->view_size());
197 AppListItemView
* folder_item_view
=
198 static_cast<AppListItemView
*>(RootViewModel()->view_at(0));
199 EXPECT_EQ(folder_item_view
->item(), folder_item
);
201 // Click on the folder to open it.
202 EXPECT_FALSE(FolderView()->visible());
203 SimulateClick(folder_item_view
);
204 base::RunLoop().RunUntilIdle();
205 EXPECT_TRUE(FolderView()->visible());
208 AppsGridViewTestApi
folder_grid_view_test_api(FolderGridView());
209 folder_grid_view_test_api
.DisableSynchronousDrag();
211 return folder_item_view
;
214 AppListItemView
* StartDragForReparent(int index_in_folder
) {
215 // Start to drag the item in folder.
216 views::View
* item_view
= FolderViewModel()->view_at(index_in_folder
);
217 gfx::Point point
= item_view
->bounds().CenterPoint();
218 AppListItemView
* dragged
=
219 SimulateInitiateDrag(FolderGridView(), AppsGridView::MOUSE
, point
);
220 EXPECT_EQ(item_view
, dragged
);
221 EXPECT_FALSE(RootGridView()->visible());
222 EXPECT_TRUE(FolderView()->visible());
224 // Drag it to top left corner.
225 point
= gfx::Point(0, 0);
226 // Two update drags needed to actually drag the view. The first changes
227 // state and the 2nd one actually moves the view. The 2nd call can be
228 // removed when UpdateDrag is fixed.
229 SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE
, dragged
, point
);
230 SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE
, dragged
, point
);
231 base::RunLoop().RunUntilIdle();
233 // Wait until the folder view is invisible and root grid view shows up.
234 GridViewVisibleWaiter(RootGridView()).Wait();
235 EXPECT_TRUE(RootGridView()->visible());
236 EXPECT_EQ(0, FolderView()->layer()->opacity());
242 views::Widget
* main_widget_
; // Owned by native window.
243 AppListMainView
* main_view_
; // Owned by |main_widget_|.
244 scoped_ptr
<AppListTestViewDelegate
> delegate_
;
245 views::Widget
* search_box_widget_
; // Owned by |main_widget_|.
246 SearchBoxView
* search_box_view_
; // Owned by |search_box_widget_|.
249 DISALLOW_COPY_AND_ASSIGN(AppListMainViewTest
);
254 // Tests changing the AppListModel when switching profiles.
255 TEST_F(AppListMainViewTest
, ModelChanged
) {
256 delegate_
->GetTestModel()->PopulateApps(kInitialItems
);
257 EXPECT_EQ(kInitialItems
, RootViewModel()->view_size());
259 // The model is owned by a profile keyed service, which is never destroyed
260 // until after profile switching.
261 scoped_ptr
<AppListModel
> old_model(delegate_
->ReleaseTestModel());
263 const int kReplacementItems
= 5;
264 delegate_
->ReplaceTestModel(kReplacementItems
);
265 main_view_
->ModelChanged();
266 EXPECT_EQ(kReplacementItems
, RootViewModel()->view_size());
269 // Tests that mouse hovering over an app item highlights it
270 TEST_F(AppListMainViewTest
, MouseHoverToHighlight
) {
271 delegate_
->GetTestModel()->PopulateApps(2);
272 main_widget_
->Show();
274 ui::test::EventGenerator
generator(GetContext(),
275 main_widget_
->GetNativeWindow());
276 AppListItemView
* item0
= RootViewModel()->view_at(0);
277 AppListItemView
* item1
= RootViewModel()->view_at(1);
279 // If experimental launcher, switch to All Apps page
280 if (app_list::switches::IsExperimentalAppListEnabled()) {
281 GetContentsView()->SetActiveState(AppListModel::STATE_APPS
);
282 GetContentsView()->Layout();
285 generator
.MoveMouseTo(item0
->GetBoundsInScreen().CenterPoint());
286 EXPECT_TRUE(item0
->is_highlighted());
287 EXPECT_FALSE(item1
->is_highlighted());
289 generator
.MoveMouseTo(item1
->GetBoundsInScreen().CenterPoint());
290 EXPECT_FALSE(item0
->is_highlighted());
291 EXPECT_TRUE(item1
->is_highlighted());
293 generator
.MoveMouseTo(gfx::Point(-1, -1));
294 EXPECT_FALSE(item0
->is_highlighted());
295 EXPECT_FALSE(item1
->is_highlighted());
298 // No touch on desktop Mac. Tracked in http://crbug.com/445520.
299 #if defined(OS_MACOSX) && !defined(USE_AURA)
300 #define MAYBE_TapGestureToHighlight DISABLED_TapGestureToHighlight
302 #define MAYBE_TapGestureToHighlight TapGestureToHighlight
305 // Tests that tap gesture on app item highlights it
306 TEST_F(AppListMainViewTest
, MAYBE_TapGestureToHighlight
) {
307 delegate_
->GetTestModel()->PopulateApps(1);
308 main_widget_
->Show();
310 ui::test::EventGenerator
generator(GetContext(),
311 main_widget_
->GetNativeWindow());
312 AppListItemView
* item
= RootViewModel()->view_at(0);
314 // If experimental launcher, switch to All Apps page
315 if (app_list::switches::IsExperimentalAppListEnabled()) {
316 GetContentsView()->SetActiveState(AppListModel::STATE_APPS
);
317 GetContentsView()->Layout();
320 generator
.set_current_location(item
->GetBoundsInScreen().CenterPoint());
321 generator
.PressTouch();
322 EXPECT_TRUE(item
->is_highlighted());
324 generator
.ReleaseTouch();
325 EXPECT_FALSE(item
->is_highlighted());
328 // Tests dragging an item out of a single item folder and drop it at the last
330 TEST_F(AppListMainViewTest
, DragLastItemFromFolderAndDropAtLastSlot
) {
331 AppListItemView
* folder_item_view
= CreateAndOpenSingleItemFolder();
332 const gfx::Rect first_slot_tile
= folder_item_view
->bounds();
334 EXPECT_EQ(1, FolderViewModel()->view_size());
336 AppListItemView
* dragged
= StartDragForReparent(0);
338 // Drop it to the slot on the right of first slot.
339 gfx::Rect
drop_target_tile(first_slot_tile
);
340 drop_target_tile
.Offset(first_slot_tile
.width() * 2, 0);
341 gfx::Point point
= drop_target_tile
.CenterPoint();
342 SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE
, dragged
, point
);
345 FolderGridView()->EndDrag(false);
347 // Folder icon view should be gone and there is only one item view.
348 EXPECT_EQ(1, RootViewModel()->view_size());
350 AppListItemView::kViewClassName
,
351 static_cast<views::View
*>(RootViewModel()->view_at(0))->GetClassName());
353 // The item view should be in slot 1 instead of slot 2 where it is dropped.
354 AppsGridViewTestApi
root_grid_view_test_api(RootGridView());
355 root_grid_view_test_api
.LayoutToIdealBounds();
356 EXPECT_EQ(first_slot_tile
, RootViewModel()->view_at(0)->bounds());
358 // Single item folder should be auto removed.
360 delegate_
->GetTestModel()->FindFolderItem("single_item_folder"));
363 // Tests dragging an item out of a single item folder and dropping it onto the
364 // page switcher. Regression test for http://crbug.com/415530/.
365 TEST_F(AppListMainViewTest
, DragReparentItemOntoPageSwitcher
) {
366 // Number of apps to populate. Should provide more than 1 page of apps (6*4 =
368 const int kNumApps
= 30;
370 // Ensure we are on the apps grid view page.
371 app_list::ContentsView
* contents_view
= GetContentsView();
372 contents_view
->SetActiveState(AppListModel::STATE_APPS
);
373 contents_view
->Layout();
375 AppListItemView
* folder_item_view
= CreateAndOpenSingleItemFolder();
376 const gfx::Rect first_slot_tile
= folder_item_view
->bounds();
378 delegate_
->GetTestModel()->PopulateApps(kNumApps
);
380 EXPECT_EQ(1, FolderViewModel()->view_size());
381 EXPECT_EQ(kNumApps
+ 1, RootViewModel()->view_size());
383 AppListItemView
* dragged
= StartDragForReparent(0);
385 gfx::Rect grid_view_bounds
= RootGridView()->bounds();
386 // Drag the reparent item to the page switcher.
388 gfx::Point(grid_view_bounds
.width() / 2,
389 grid_view_bounds
.bottom() - first_slot_tile
.height());
390 SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE
, dragged
, point
);
393 FolderGridView()->EndDrag(false);
395 // The folder should be destroyed.
396 EXPECT_EQ(kNumApps
+ 1, RootViewModel()->view_size());
398 delegate_
->GetTestModel()->FindFolderItem("single_item_folder"));
401 // Test that an interrupted drag while reparenting an item from a folder, when
402 // canceled via the root grid, correctly forwards the cancelation to the drag
403 // ocurring from the folder.
404 TEST_F(AppListMainViewTest
, MouseDragItemOutOfFolderWithCancel
) {
405 CreateAndOpenSingleItemFolder();
406 AppListItemView
* dragged
= StartDragForReparent(0);
408 // Now add an item to the model, not in any folder, e.g., as if by Sync.
409 EXPECT_TRUE(RootGridView()->has_dragged_view());
410 EXPECT_TRUE(FolderGridView()->has_dragged_view());
411 delegate_
->GetTestModel()->CreateAndAddItem("Extra");
413 // The drag operation should get canceled.
414 EXPECT_FALSE(RootGridView()->has_dragged_view());
415 EXPECT_FALSE(FolderGridView()->has_dragged_view());
417 // Additional mouse move operations should be ignored.
418 gfx::Point
point(1, 1);
419 SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE
, dragged
, point
);
420 EXPECT_FALSE(RootGridView()->has_dragged_view());
421 EXPECT_FALSE(FolderGridView()->has_dragged_view());
424 // Test that dragging an app out of a single item folder and reparenting it
425 // back into its original folder results in a cancelled reparent. This is a
426 // regression test for http://crbug.com/429083.
427 TEST_F(AppListMainViewTest
, ReparentSingleItemOntoSelf
) {
428 // Add a folder with 1 item.
429 AppListItemView
* folder_item_view
= CreateAndOpenSingleItemFolder();
430 std::string folder_id
= folder_item_view
->item()->id();
432 // Add another top level app.
433 delegate_
->GetTestModel()->PopulateApps(1);
434 gfx::Point drag_point
= folder_item_view
->bounds().CenterPoint();
436 views::View::ConvertPointToTarget(RootGridView(), FolderGridView(),
439 AppListItemView
* dragged
= StartDragForReparent(0);
441 // Drag the reparent item back into its folder.
442 SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE
, dragged
,
444 FolderGridView()->EndDrag(false);
446 // The app list model should remain unchanged.
447 EXPECT_EQ(1, FolderViewModel()->view_size());
448 EXPECT_EQ(2, RootViewModel()->view_size());
449 EXPECT_EQ(folder_id
, RootGridView()->GetItemViewAt(0)->item()->id());
451 delegate_
->GetTestModel()->FindFolderItem("single_item_folder"));
455 } // namespace app_list