blob: 9bbe784f230562a674e5dd6897e05ffd2fe10cd8 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/app_list/views/app_list_main_view.h"
#include <memory>
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/app_list/app_list_switches.h"
#include "ui/app_list/test/app_list_test_model.h"
#include "ui/app_list/test/app_list_test_view_delegate.h"
#include "ui/app_list/views/app_list_folder_view.h"
#include "ui/app_list/views/app_list_item_view.h"
#include "ui/app_list/views/apps_container_view.h"
#include "ui/app_list/views/apps_grid_view.h"
#include "ui/app_list/views/contents_view.h"
#include "ui/app_list/views/search_box_view.h"
#include "ui/app_list/views/test/apps_grid_view_test_api.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view_model.h"
#include "ui/views/widget/widget.h"
namespace app_list {
namespace test {
namespace {
const int kInitialItems = 2;
class GridViewVisibleWaiter {
public:
explicit GridViewVisibleWaiter(AppsGridView* grid_view)
: grid_view_(grid_view) {}
~GridViewVisibleWaiter() {}
void Wait() {
if (grid_view_->visible())
return;
check_timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(50),
base::Bind(&GridViewVisibleWaiter::OnTimerCheck,
base::Unretained(this)));
run_loop_.reset(new base::RunLoop);
run_loop_->Run();
check_timer_.Stop();
}
private:
void OnTimerCheck() {
if (grid_view_->visible())
run_loop_->Quit();
}
AppsGridView* grid_view_;
std::unique_ptr<base::RunLoop> run_loop_;
base::RepeatingTimer check_timer_;
DISALLOW_COPY_AND_ASSIGN(GridViewVisibleWaiter);
};
class AppListMainViewTest : public views::ViewsTestBase {
public:
AppListMainViewTest()
: main_widget_(nullptr),
main_view_(nullptr),
search_box_widget_(nullptr),
search_box_view_(nullptr) {}
~AppListMainViewTest() override {}
// testing::Test overrides:
void SetUp() override {
views::ViewsTestBase::SetUp();
delegate_.reset(new AppListTestViewDelegate);
// In Ash, the third argument is a container aura::Window, but it is always
// NULL on Windows, and not needed for tests. It is only used to determine
// the scale factor for preloading icons.
main_view_ = new AppListMainView(delegate_.get());
main_view_->SetPaintToLayer(true);
main_view_->model()->SetFoldersEnabled(true);
search_box_view_ = new SearchBoxView(main_view_, delegate_.get());
main_view_->Init(nullptr, 0, search_box_view_);
main_widget_ = new views::Widget;
views::Widget::InitParams main_widget_params =
CreateParams(views::Widget::InitParams::TYPE_POPUP);
main_widget_params.bounds.set_size(main_view_->GetPreferredSize());
main_widget_->Init(main_widget_params);
main_widget_->SetContentsView(main_view_);
search_box_widget_ = new views::Widget;
views::Widget::InitParams search_box_widget_params =
CreateParams(views::Widget::InitParams::TYPE_CONTROL);
search_box_widget_params.parent = main_widget_->GetNativeView();
search_box_widget_params.opacity =
views::Widget::InitParams::TRANSLUCENT_WINDOW;
search_box_widget_->Init(search_box_widget_params);
search_box_widget_->SetContentsView(search_box_view_);
}
void TearDown() override {
main_widget_->Close();
views::ViewsTestBase::TearDown();
delegate_.reset();
}
// |point| is in |grid_view|'s coordinates.
AppListItemView* GetItemViewAtPointInGrid(AppsGridView* grid_view,
const gfx::Point& point) {
const views::ViewModelT<AppListItemView>* view_model =
grid_view->view_model_for_test();
for (int i = 0; i < view_model->view_size(); ++i) {
views::View* view = view_model->view_at(i);
if (view->bounds().Contains(point)) {
return static_cast<AppListItemView*>(view);
}
}
return NULL;
}
void SimulateClick(views::View* view) {
gfx::Point center = view->GetLocalBounds().CenterPoint();
view->OnMousePressed(ui::MouseEvent(
ui::ET_MOUSE_PRESSED, center, center, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON));
view->OnMouseReleased(ui::MouseEvent(
ui::ET_MOUSE_RELEASED, center, center, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON));
}
// |point| is in |grid_view|'s coordinates.
AppListItemView* SimulateInitiateDrag(AppsGridView* grid_view,
AppsGridView::Pointer pointer,
const gfx::Point& point) {
AppListItemView* view = GetItemViewAtPointInGrid(grid_view, point);
DCHECK(view);
gfx::Point translated =
gfx::PointAtOffsetFromOrigin(point - view->bounds().origin());
ui::MouseEvent pressed_event(ui::ET_MOUSE_PRESSED, translated, point,
ui::EventTimeForNow(), 0, 0);
grid_view->InitiateDrag(view, pointer, pressed_event);
return view;
}
// |point| is in |grid_view|'s coordinates.
void SimulateUpdateDrag(AppsGridView* grid_view,
AppsGridView::Pointer pointer,
AppListItemView* drag_view,
const gfx::Point& point) {
DCHECK(drag_view);
gfx::Point translated =
gfx::PointAtOffsetFromOrigin(point - drag_view->bounds().origin());
ui::MouseEvent drag_event(ui::ET_MOUSE_DRAGGED, translated, point,
ui::EventTimeForNow(), 0, 0);
grid_view->UpdateDragFromItem(pointer, drag_event);
}
ContentsView* GetContentsView() { return main_view_->contents_view(); }
AppsGridView* RootGridView() {
return GetContentsView()->apps_container_view()->apps_grid_view();
}
AppListFolderView* FolderView() {
return GetContentsView()->apps_container_view()->app_list_folder_view();
}
AppsGridView* FolderGridView() { return FolderView()->items_grid_view(); }
const views::ViewModelT<AppListItemView>* RootViewModel() {
return RootGridView()->view_model_for_test();
}
const views::ViewModelT<AppListItemView>* FolderViewModel() {
return FolderGridView()->view_model_for_test();
}
AppListItemView* CreateAndOpenSingleItemFolder() {
// Prepare single folder with a single item in it.
AppListFolderItem* folder_item =
delegate_->GetTestModel()->CreateSingleItemFolder("single_item_folder",
"single");
EXPECT_EQ(folder_item,
delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
EXPECT_EQ(AppListFolderItem::kItemType, folder_item->GetItemType());
EXPECT_EQ(1, RootViewModel()->view_size());
AppListItemView* folder_item_view =
static_cast<AppListItemView*>(RootViewModel()->view_at(0));
EXPECT_EQ(folder_item_view->item(), folder_item);
// Click on the folder to open it.
EXPECT_FALSE(FolderView()->visible());
SimulateClick(folder_item_view);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(FolderView()->visible());
#if defined(OS_WIN)
AppsGridViewTestApi folder_grid_view_test_api(FolderGridView());
folder_grid_view_test_api.DisableSynchronousDrag();
#endif
return folder_item_view;
}
AppListItemView* StartDragForReparent(int index_in_folder) {
// Start to drag the item in folder.
views::View* item_view = FolderViewModel()->view_at(index_in_folder);
gfx::Point point = item_view->bounds().CenterPoint();
AppListItemView* dragged =
SimulateInitiateDrag(FolderGridView(), AppsGridView::MOUSE, point);
EXPECT_EQ(item_view, dragged);
EXPECT_FALSE(RootGridView()->visible());
EXPECT_TRUE(FolderView()->visible());
// Drag it to top left corner.
point = gfx::Point(0, 0);
// Two update drags needed to actually drag the view. The first changes
// state and the 2nd one actually moves the view. The 2nd call can be
// removed when UpdateDrag is fixed.
SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
base::RunLoop().RunUntilIdle();
// Wait until the folder view is invisible and root grid view shows up.
GridViewVisibleWaiter(RootGridView()).Wait();
EXPECT_TRUE(RootGridView()->visible());
EXPECT_EQ(0, FolderView()->layer()->opacity());
return dragged;
}
protected:
views::Widget* main_widget_; // Owned by native window.
AppListMainView* main_view_; // Owned by |main_widget_|.
std::unique_ptr<AppListTestViewDelegate> delegate_;
views::Widget* search_box_widget_; // Owned by |main_widget_|.
SearchBoxView* search_box_view_; // Owned by |search_box_widget_|.
private:
DISALLOW_COPY_AND_ASSIGN(AppListMainViewTest);
};
} // namespace
// Tests changing the AppListModel when switching profiles.
TEST_F(AppListMainViewTest, ModelChanged) {
delegate_->GetTestModel()->PopulateApps(kInitialItems);
EXPECT_EQ(kInitialItems, RootViewModel()->view_size());
// The model is owned by a profile keyed service, which is never destroyed
// until after profile switching.
std::unique_ptr<AppListModel> old_model(delegate_->ReleaseTestModel());
const int kReplacementItems = 5;
delegate_->ReplaceTestModel(kReplacementItems);
main_view_->ModelChanged();
EXPECT_EQ(kReplacementItems, RootViewModel()->view_size());
}
// Tests that mouse hovering over an app item highlights it
TEST_F(AppListMainViewTest, MouseHoverToHighlight) {
delegate_->GetTestModel()->PopulateApps(2);
main_widget_->Show();
ui::test::EventGenerator generator(GetContext(),
main_widget_->GetNativeWindow());
AppListItemView* item0 = RootViewModel()->view_at(0);
AppListItemView* item1 = RootViewModel()->view_at(1);
// If experimental launcher, switch to All Apps page
if (app_list::switches::IsExperimentalAppListEnabled()) {
GetContentsView()->SetActiveState(AppListModel::STATE_APPS);
GetContentsView()->Layout();
}
generator.MoveMouseTo(item0->GetBoundsInScreen().CenterPoint());
EXPECT_TRUE(item0->is_highlighted());
EXPECT_FALSE(item1->is_highlighted());
generator.MoveMouseTo(item1->GetBoundsInScreen().CenterPoint());
EXPECT_FALSE(item0->is_highlighted());
EXPECT_TRUE(item1->is_highlighted());
generator.MoveMouseTo(gfx::Point(-1, -1));
EXPECT_FALSE(item0->is_highlighted());
EXPECT_FALSE(item1->is_highlighted());
}
// No touch on desktop Mac. Tracked in http://crbug.com/445520.
#if defined(OS_MACOSX) && !defined(USE_AURA)
#define MAYBE_TapGestureToHighlight DISABLED_TapGestureToHighlight
#else
#define MAYBE_TapGestureToHighlight TapGestureToHighlight
#endif
// Tests that tap gesture on app item highlights it
TEST_F(AppListMainViewTest, MAYBE_TapGestureToHighlight) {
delegate_->GetTestModel()->PopulateApps(1);
main_widget_->Show();
ui::test::EventGenerator generator(GetContext(),
main_widget_->GetNativeWindow());
AppListItemView* item = RootViewModel()->view_at(0);
// If experimental launcher, switch to All Apps page
if (app_list::switches::IsExperimentalAppListEnabled()) {
GetContentsView()->SetActiveState(AppListModel::STATE_APPS);
GetContentsView()->Layout();
}
generator.set_current_location(item->GetBoundsInScreen().CenterPoint());
generator.PressTouch();
EXPECT_TRUE(item->is_highlighted());
generator.ReleaseTouch();
EXPECT_FALSE(item->is_highlighted());
}
// Tests dragging an item out of a single item folder and drop it at the last
// slot.
TEST_F(AppListMainViewTest, DragLastItemFromFolderAndDropAtLastSlot) {
AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
const gfx::Rect first_slot_tile = folder_item_view->bounds();
EXPECT_EQ(1, FolderViewModel()->view_size());
AppListItemView* dragged = StartDragForReparent(0);
// Drop it to the slot on the right of first slot.
gfx::Rect drop_target_tile(first_slot_tile);
drop_target_tile.Offset(first_slot_tile.width() * 2, 0);
gfx::Point point = drop_target_tile.CenterPoint();
SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
// Drop it.
FolderGridView()->EndDrag(false);
// Folder icon view should be gone and there is only one item view.
EXPECT_EQ(1, RootViewModel()->view_size());
EXPECT_EQ(
AppListItemView::kViewClassName,
static_cast<views::View*>(RootViewModel()->view_at(0))->GetClassName());
// The item view should be in slot 1 instead of slot 2 where it is dropped.
AppsGridViewTestApi root_grid_view_test_api(RootGridView());
root_grid_view_test_api.LayoutToIdealBounds();
EXPECT_EQ(first_slot_tile, RootViewModel()->view_at(0)->bounds());
// Single item folder should be auto removed.
EXPECT_EQ(NULL,
delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
// Ensure keyboard selection works on the root grid view after a reparent.
// This is a regression test for https://crbug.com/466058.
ui::KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_RIGHT, ui::EF_NONE);
GetContentsView()->apps_container_view()->OnKeyPressed(key_event);
EXPECT_TRUE(RootGridView()->has_selected_view());
EXPECT_FALSE(FolderGridView()->has_selected_view());
}
// Tests dragging an item out of a single item folder and dropping it onto the
// page switcher. Regression test for http://crbug.com/415530/.
TEST_F(AppListMainViewTest, DragReparentItemOntoPageSwitcher) {
// Number of apps to populate. Should provide more than 1 page of apps (6*4 =
// 24).
const int kNumApps = 30;
// Ensure we are on the apps grid view page.
app_list::ContentsView* contents_view = GetContentsView();
contents_view->SetActiveState(AppListModel::STATE_APPS);
contents_view->Layout();
AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
const gfx::Rect first_slot_tile = folder_item_view->bounds();
delegate_->GetTestModel()->PopulateApps(kNumApps);
EXPECT_EQ(1, FolderViewModel()->view_size());
EXPECT_EQ(kNumApps + 1, RootViewModel()->view_size());
AppListItemView* dragged = StartDragForReparent(0);
gfx::Rect grid_view_bounds = RootGridView()->bounds();
// Drag the reparent item to the page switcher.
gfx::Point point =
gfx::Point(grid_view_bounds.width() / 2,
grid_view_bounds.bottom() - first_slot_tile.height());
SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
// Drop it.
FolderGridView()->EndDrag(false);
// The folder should be destroyed.
EXPECT_EQ(kNumApps + 1, RootViewModel()->view_size());
EXPECT_EQ(NULL,
delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
}
// Test that an interrupted drag while reparenting an item from a folder, when
// canceled via the root grid, correctly forwards the cancelation to the drag
// ocurring from the folder.
TEST_F(AppListMainViewTest, MouseDragItemOutOfFolderWithCancel) {
CreateAndOpenSingleItemFolder();
AppListItemView* dragged = StartDragForReparent(0);
// Now add an item to the model, not in any folder, e.g., as if by Sync.
EXPECT_TRUE(RootGridView()->has_dragged_view());
EXPECT_TRUE(FolderGridView()->has_dragged_view());
delegate_->GetTestModel()->CreateAndAddItem("Extra");
// The drag operation should get canceled.
EXPECT_FALSE(RootGridView()->has_dragged_view());
EXPECT_FALSE(FolderGridView()->has_dragged_view());
// Additional mouse move operations should be ignored.
gfx::Point point(1, 1);
SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged, point);
EXPECT_FALSE(RootGridView()->has_dragged_view());
EXPECT_FALSE(FolderGridView()->has_dragged_view());
}
// Test that dragging an app out of a single item folder and reparenting it
// back into its original folder results in a cancelled reparent. This is a
// regression test for http://crbug.com/429083.
TEST_F(AppListMainViewTest, ReparentSingleItemOntoSelf) {
// Add a folder with 1 item.
AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder();
std::string folder_id = folder_item_view->item()->id();
// Add another top level app.
delegate_->GetTestModel()->PopulateApps(1);
gfx::Point drag_point = folder_item_view->bounds().CenterPoint();
views::View::ConvertPointToTarget(RootGridView(), FolderGridView(),
&drag_point);
AppListItemView* dragged = StartDragForReparent(0);
// Drag the reparent item back into its folder.
SimulateUpdateDrag(FolderGridView(), AppsGridView::MOUSE, dragged,
drag_point);
FolderGridView()->EndDrag(false);
// The app list model should remain unchanged.
EXPECT_EQ(1, FolderViewModel()->view_size());
EXPECT_EQ(2, RootViewModel()->view_size());
EXPECT_EQ(folder_id, RootGridView()->GetItemViewAt(0)->item()->id());
EXPECT_NE(nullptr,
delegate_->GetTestModel()->FindFolderItem("single_item_folder"));
}
} // namespace test
} // namespace app_list