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 24 namespace app_list { 25 namespace test { 26 27 namespace { 28 29 const int kInitialItems = 2; 30 31 class 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 63 class 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. 238 TEST_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. 254 TEST_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/. 288 TEST_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. 318 TEST_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