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.
4
5#include "ui/app_list/views/app_list_main_view.h"
6
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/test/app_list_test_model.h"
13#include "ui/app_list/test/app_list_test_view_delegate.h"
14#include "ui/app_list/views/app_list_folder_view.h"
15#include "ui/app_list/views/app_list_item_view.h"
16#include "ui/app_list/views/apps_container_view.h"
17#include "ui/app_list/views/apps_grid_view.h"
18#include "ui/app_list/views/contents_view.h"
19#include "ui/app_list/views/test/apps_grid_view_test_api.h"
20#include "ui/views/test/views_test_base.h"
21#include "ui/views/view_model.h"
22#include "ui/views/widget/widget.h"
23
24namespace app_list {
25namespace test {
26
27namespace {
28
29const int kInitialItems = 2;
30
31class GridViewVisibleWaiter {
32 public:
33  explicit GridViewVisibleWaiter(AppsGridView* grid_view)
34      : grid_view_(grid_view) {}
35  ~GridViewVisibleWaiter() {}
36
37  void Wait() {
38    if (grid_view_->visible())
39      return;
40
41    check_timer_.Start(FROM_HERE,
42                       base::TimeDelta::FromMilliseconds(50),
43                       base::Bind(&GridViewVisibleWaiter::OnTimerCheck,
44                                  base::Unretained(this)));
45    run_loop_.reset(new base::RunLoop);
46    run_loop_->Run();
47    check_timer_.Stop();
48  }
49
50 private:
51  void OnTimerCheck() {
52    if (grid_view_->visible())
53      run_loop_->Quit();
54  }
55
56  AppsGridView* grid_view_;
57  scoped_ptr<base::RunLoop> run_loop_;
58  base::RepeatingTimer<GridViewVisibleWaiter> check_timer_;
59
60  DISALLOW_COPY_AND_ASSIGN(GridViewVisibleWaiter);
61};
62
63class AppListMainViewTest : public views::ViewsTestBase {
64 public:
65  AppListMainViewTest()
66      : widget_(NULL),
67        main_view_(NULL) {}
68
69  virtual ~AppListMainViewTest() {}
70
71  // testing::Test overrides:
72  virtual void SetUp() OVERRIDE {
73    views::ViewsTestBase::SetUp();
74    delegate_.reset(new AppListTestViewDelegate);
75
76    // In Ash, the third argument is a container aura::Window, but it is always
77    // NULL on Windows, and not needed for tests. It is only used to determine
78    // the scale factor for preloading icons.
79    main_view_ = new AppListMainView(delegate_.get(), 0, NULL);
80    main_view_->SetPaintToLayer(true);
81    main_view_->model()->SetFoldersEnabled(true);
82
83    widget_ = new views::Widget;
84    views::Widget::InitParams params =
85        CreateParams(views::Widget::InitParams::TYPE_POPUP);
86    params.bounds.set_size(main_view_->GetPreferredSize());
87    widget_->Init(params);
88
89    widget_->SetContentsView(main_view_);
90  }
91
92  virtual void TearDown() OVERRIDE {
93    widget_->Close();
94    views::ViewsTestBase::TearDown();
95    delegate_.reset();
96  }
97
98  // |point| is in |grid_view|'s coordinates.
99  AppListItemView* GetItemViewAtPointInGrid(AppsGridView* grid_view,
100                                            const gfx::Point& point) {
101    const views::ViewModel* view_model = grid_view->view_model_for_test();
102    for (int i = 0; i < view_model->view_size(); ++i) {
103      views::View* view = view_model->view_at(i);
104      if (view->bounds().Contains(point)) {
105        return static_cast<AppListItemView*>(view);
106      }
107    }
108
109    return NULL;
110  }
111
112  void SimulateClick(views::View* view) {
113    gfx::Point center = view->GetLocalBounds().CenterPoint();
114    view->OnMousePressed(ui::MouseEvent(ui::ET_MOUSE_PRESSED,
115                                        center,
116                                        center,
117                                        ui::EF_LEFT_MOUSE_BUTTON,
118                                        ui::EF_LEFT_MOUSE_BUTTON));
119    view->OnMouseReleased(ui::MouseEvent(ui::ET_MOUSE_RELEASED,
120                                         center,
121                                         center,
122                                         ui::EF_LEFT_MOUSE_BUTTON,
123                                         ui::EF_LEFT_MOUSE_BUTTON));
124  }
125
126  // |point| is in |grid_view|'s coordinates.
127  AppListItemView* SimulateInitiateDrag(AppsGridView* grid_view,
128                                        AppsGridView::Pointer pointer,
129                                        const gfx::Point& point) {
130    AppListItemView* view = GetItemViewAtPointInGrid(grid_view, point);
131    DCHECK(view);
132
133    gfx::Point translated =
134        gfx::PointAtOffsetFromOrigin(point - view->bounds().origin());
135    ui::MouseEvent pressed_event(ui::ET_MOUSE_PRESSED, translated, point, 0, 0);
136    grid_view->InitiateDrag(view, pointer, pressed_event);
137    return view;
138  }
139
140  // |point| is in |grid_view|'s coordinates.
141  void SimulateUpdateDrag(AppsGridView* grid_view,
142                          AppsGridView::Pointer pointer,
143                          AppListItemView* drag_view,
144                          const gfx::Point& point) {
145    DCHECK(drag_view);
146    gfx::Point translated =
147        gfx::PointAtOffsetFromOrigin(point - drag_view->bounds().origin());
148    ui::MouseEvent drag_event(ui::ET_MOUSE_DRAGGED, translated, point, 0, 0);
149    grid_view->UpdateDragFromItem(pointer, drag_event);
150  }
151
152  AppsGridView* RootGridView() {
153    return main_view_->contents_view()->apps_container_view()->apps_grid_view();
154  }
155
156  AppListFolderView* FolderView() {
157    return main_view_->contents_view()
158        ->apps_container_view()
159        ->app_list_folder_view();
160  }
161
162  AppsGridView* FolderGridView() { return FolderView()->items_grid_view(); }
163
164  const views::ViewModel* RootViewModel() {
165    return RootGridView()->view_model_for_test();
166  }
167
168  const views::ViewModel* FolderViewModel() {
169    return FolderGridView()->view_model_for_test();
170  }
171
172  AppListItemView* CreateAndOpenSingleItemFolder() {
173    // Prepare single folder with a single item in it.
174    AppListFolderItem* folder_item =
175        delegate_->GetTestModel()->CreateSingleItemFolder("single_item_folder",
176                                                          "single");
177    EXPECT_EQ(folder_item,
178              delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
179    EXPECT_EQ(AppListFolderItem::kItemType, folder_item->GetItemType());
180
181    EXPECT_EQ(1, RootViewModel()->view_size());
182    AppListItemView* folder_item_view =
183        static_cast<AppListItemView*>(RootViewModel()->view_at(0));
184    EXPECT_EQ(folder_item_view->item(), folder_item);
185
186    // Click on the folder to open it.
187    EXPECT_FALSE(FolderView()->visible());
188    SimulateClick(folder_item_view);
189    base::RunLoop().RunUntilIdle();
190    EXPECT_TRUE(FolderView()->visible());
191
192#if defined(OS_WIN)
193    AppsGridViewTestApi folder_grid_view_test_api(FolderGridView());
194    folder_grid_view_test_api.DisableSynchronousDrag();
195#endif
196    return folder_item_view;
197  }
198
199  AppListItemView* StartDragForReparent(int index_in_folder) {
200    // Start to drag the item in folder.
201    views::View* item_view = FolderViewModel()->view_at(index_in_folder);
202    gfx::Point point = item_view->bounds().CenterPoint();
203    AppListItemView* dragged =
204        SimulateInitiateDrag(FolderGridView(), AppsGridView::MOUSE, point);
205    EXPECT_EQ(item_view, dragged);
206    EXPECT_FALSE(RootGridView()->visible());
207    EXPECT_TRUE(FolderView()->visible());
208
209    // Drag it to top left corner.
210    point = gfx::Point(0, 0);
211    // Two update drags needed to actually drag the view. The first changes
212    // state and the 2nd one actually moves the view. The 2nd call can be
213    // removed when UpdateDrag is fixed.
214    SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
215    SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
216    base::RunLoop().RunUntilIdle();
217
218    // Wait until the folder view is invisible and root grid view shows up.
219    GridViewVisibleWaiter(RootGridView()).Wait();
220    EXPECT_TRUE(RootGridView()->visible());
221    EXPECT_EQ(0, FolderView()->layer()->opacity());
222
223    return dragged;
224  }
225
226 protected:
227  views::Widget* widget_;  // Owned by native window.
228  AppListMainView* main_view_;  // Owned by |widget_|.
229  scoped_ptr<AppListTestViewDelegate> delegate_;
230
231 private:
232  DISALLOW_COPY_AND_ASSIGN(AppListMainViewTest);
233};
234
235}  // namespace
236
237// Tests changing the AppListModel when switching profiles.
238TEST_F(AppListMainViewTest, ModelChanged) {
239  delegate_->GetTestModel()->PopulateApps(kInitialItems);
240  EXPECT_EQ(kInitialItems, RootViewModel()->view_size());
241
242  // The model is owned by a profile keyed service, which is never destroyed
243  // until after profile switching.
244  scoped_ptr<AppListModel> old_model(delegate_->ReleaseTestModel());
245
246  const int kReplacementItems = 5;
247  delegate_->ReplaceTestModel(kReplacementItems);
248  main_view_->ModelChanged();
249  EXPECT_EQ(kReplacementItems, RootViewModel()->view_size());
250}
251
252// Tests dragging an item out of a single item folder and drop it at the last
253// slot.
254TEST_F(AppListMainViewTest, DragLastItemFromFolderAndDropAtLastSlot) {
255  AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
256  const gfx::Rect first_slot_tile = folder_item_view->bounds();
257
258  EXPECT_EQ(1, FolderViewModel()->view_size());
259
260  AppListItemView* dragged = StartDragForReparent(0);
261
262  // Drop it to the slot on the right of first slot.
263  gfx::Rect drop_target_tile(first_slot_tile);
264  drop_target_tile.Offset(first_slot_tile.width() * 2, 0);
265  gfx::Point point = drop_target_tile.CenterPoint();
266  SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
267
268  // Drop it.
269  FolderGridView()->EndDrag(false);
270
271  // Folder icon view should be gone and there is only one item view.
272  EXPECT_EQ(1, RootViewModel()->view_size());
273  EXPECT_EQ(AppListItemView::kViewClassName,
274            RootViewModel()->view_at(0)->GetClassName());
275
276  // The item view should be in slot 1 instead of slot 2 where it is dropped.
277  AppsGridViewTestApi root_grid_view_test_api(RootGridView());
278  root_grid_view_test_api.LayoutToIdealBounds();
279  EXPECT_EQ(first_slot_tile, RootViewModel()->view_at(0)->bounds());
280
281  // Single item folder should be auto removed.
282  EXPECT_EQ(NULL,
283            delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
284}
285
286// Tests dragging an item out of a single item folder and dropping it onto the
287// page switcher. Regression test for http://crbug.com/415530/.
288TEST_F(AppListMainViewTest, DragReparentItemOntoPageSwitcher) {
289  AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
290  const gfx::Rect first_slot_tile = folder_item_view->bounds();
291
292  delegate_->GetTestModel()->PopulateApps(20);
293
294  EXPECT_EQ(1, FolderViewModel()->view_size());
295  EXPECT_EQ(21, RootViewModel()->view_size());
296
297  AppListItemView* dragged = StartDragForReparent(0);
298
299  gfx::Rect main_view_bounds = main_view_->bounds();
300  // Drag the reparent item to the page switcher.
301  gfx::Point point =
302      gfx::Point(main_view_bounds.width() / 2,
303                 main_view_bounds.bottom() - first_slot_tile.height());
304  SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
305
306  // Drop it.
307  FolderGridView()->EndDrag(false);
308
309  // The folder should be destroyed.
310  EXPECT_EQ(21, RootViewModel()->view_size());
311  EXPECT_EQ(NULL,
312            delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
313}
314
315// Test that an interrupted drag while reparenting an item from a folder, when
316// canceled via the root grid, correctly forwards the cancelation to the drag
317// ocurring from the folder.
318TEST_F(AppListMainViewTest, MouseDragItemOutOfFolderWithCancel) {
319  CreateAndOpenSingleItemFolder();
320  AppListItemView* dragged = StartDragForReparent(0);
321
322  // Now add an item to the model, not in any folder, e.g., as if by Sync.
323  EXPECT_TRUE(RootGridView()->has_dragged_view());
324  EXPECT_TRUE(FolderGridView()->has_dragged_view());
325  delegate_->GetTestModel()->CreateAndAddItem("Extra");
326
327  // The drag operation should get canceled.
328  EXPECT_FALSE(RootGridView()->has_dragged_view());
329  EXPECT_FALSE(FolderGridView()->has_dragged_view());
330
331  // Additional mouse move operations should be ignored.
332  gfx::Point point(1, 1);
333  SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
334  EXPECT_FALSE(RootGridView()->has_dragged_view());
335  EXPECT_FALSE(FolderGridView()->has_dragged_view());
336}
337
338}  // namespace test
339}  // namespace app_list
340