| // 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 "ash/app_list/views/app_list_main_view.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "ash/app_list/app_list_test_view_delegate.h" |
| #include "ash/app_list/model/app_list_test_model.h" |
| #include "ash/app_list/views/app_list_folder_view.h" |
| #include "ash/app_list/views/app_list_item_view.h" |
| #include "ash/app_list/views/app_list_view.h" |
| #include "ash/app_list/views/apps_container_view.h" |
| #include "ash/app_list/views/apps_grid_view.h" |
| #include "ash/app_list/views/apps_grid_view_test_api.h" |
| #include "ash/app_list/views/contents_view.h" |
| #include "ash/app_list/views/page_switcher.h" |
| #include "ash/app_list/views/paged_apps_grid_view.h" |
| #include "ash/app_list/views/search_box_view.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/app_list/app_list_features.h" |
| #include "ash/public/cpp/test/test_app_list_color_provider.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/keycodes/keyboard_codes_posix.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/test/button_test_api.h" |
| #include "ui/views/test/views_test_base.h" |
| #include "ui/views/view_model.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| namespace { |
| |
| const int kInitialItems = 2; |
| |
| } // namespace |
| |
| // Parameterized by ProductivityLauncher. |
| class AppListMainViewTest : public views::ViewsTestBase, |
| public testing::WithParamInterface<bool> { |
| public: |
| AppListMainViewTest() { |
| feature_list_.InitWithFeatureState(features::kProductivityLauncher, |
| GetParam()); |
| } |
| AppListMainViewTest(const AppListMainViewTest& other) = delete; |
| AppListMainViewTest& operator=(const AppListMainViewTest& other) = delete; |
| ~AppListMainViewTest() override = default; |
| |
| // testing::Test overrides: |
| void SetUp() override { |
| views::ViewsTestBase::SetUp(); |
| zero_duration_mode_ = |
| std::make_unique<ui::ScopedAnimationDurationScaleMode>( |
| ui::ScopedAnimationDurationScaleMode::ZERO_DURATION); |
| |
| // Create, and show the app list is fullscreen apps grid state. |
| delegate_ = std::make_unique<test::AppListTestViewDelegate>(); |
| app_list_view_ = new AppListView(delegate_.get()); |
| app_list_view_->InitView(GetContext()); |
| app_list_view_->Show(AppListViewState::kFullscreenAllApps, |
| /*is_side_shelf=*/false); |
| EXPECT_TRUE(app_list_view_->GetWidget()->IsVisible()); |
| } |
| |
| void TearDown() override { |
| app_list_view_->GetWidget()->Close(); |
| zero_duration_mode_.reset(); |
| views::ViewsTestBase::TearDown(); |
| } |
| |
| // |point| is in |grid_view|'s coordinates. |
| AppListItemView* GetItemViewAtPointInGrid(AppsGridView* grid_view, |
| const gfx::Point& point) { |
| const auto& entries = grid_view->view_model()->entries(); |
| const auto iter = std::find_if( |
| entries.begin(), entries.end(), [&point](const auto& entry) { |
| return entry.view->bounds().Contains(point); |
| }); |
| return iter == entries.end() ? nullptr |
| : static_cast<AppListItemView*>(iter->view); |
| } |
| |
| void SimulateKeyPress(ui::KeyboardCode key_code) { |
| ui::KeyEvent key_press(ui::ET_KEY_PRESSED, key_code, ui::EF_NONE); |
| app_list_view_->GetWidget()->OnKeyEvent(&key_press); |
| |
| ui::KeyEvent key_release(ui::ET_KEY_RELEASED, key_code, ui::EF_NONE); |
| app_list_view_->GetWidget()->OnKeyEvent(&key_release); |
| } |
| |
| void SimulateClick(views::View* view) { |
| gfx::Point center = view->GetLocalBounds().CenterPoint(); |
| views::View::ConvertPointToWidget(view, ¢er); |
| |
| ui::MouseEvent press_event(ui::ET_MOUSE_PRESSED, center, center, |
| ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, |
| ui::EF_RIGHT_MOUSE_BUTTON); |
| view->GetWidget()->OnMouseEvent(&press_event); |
| |
| ui::MouseEvent release_event( |
| ui::ET_MOUSE_RELEASED, center, center, ui::EventTimeForNow(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_RIGHT_MOUSE_BUTTON); |
| view->GetWidget()->OnMouseEvent(&release_event); |
| } |
| |
| // |point| is in |grid_view|'s coordinates. |
| AppListItemView* SimulateInitiateDrag(AppsGridView* grid_view, |
| const gfx::Point& point) { |
| AppListItemView* view = GetItemViewAtPointInGrid(grid_view, point); |
| DCHECK(view); |
| |
| // NOTE: Assumes that the app list view window bounds match the root window |
| // bounds. |
| gfx::Point root_window_point = point; |
| views::View::ConvertPointToWidget(grid_view, &root_window_point); |
| |
| view->InitiateDrag(point, root_window_point); |
| 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); |
| |
| // NOTE: Assumes that the app list view window bounds match the root window |
| // bounds. |
| gfx::Point root_window_point = point; |
| views::View::ConvertPointToWidget(grid_view, &root_window_point); |
| |
| ui::MouseEvent drag_event(ui::ET_MOUSE_DRAGGED, point, root_window_point, |
| ui::EventTimeForNow(), 0, 0); |
| |
| grid_view->UpdateDragFromItem(pointer, drag_event); |
| } |
| |
| AppListMainView* main_view() { return app_list_view_->app_list_main_view(); } |
| |
| ContentsView* contents_view() { return main_view()->contents_view(); } |
| |
| SearchBoxView* search_box_view() { return main_view()->search_box_view(); } |
| |
| AppsGridView* GetRootGridView() { |
| return contents_view()->apps_container_view()->apps_grid_view(); |
| } |
| |
| AppListFolderView* GetFolderView() { |
| return contents_view()->apps_container_view()->app_list_folder_view(); |
| } |
| |
| PageSwitcher* GetPageSwitcherView() { |
| return contents_view()->apps_container_view()->page_switcher(); |
| } |
| |
| AppsGridView* GetFolderGridView() { |
| return GetFolderView()->items_grid_view(); |
| } |
| |
| const views::ViewModelT<AppListItemView>* GetRootViewModel() { |
| return GetRootGridView()->view_model(); |
| } |
| |
| const views::ViewModelT<AppListItemView>* GetFolderViewModel() { |
| return GetFolderGridView()->view_model(); |
| } |
| |
| AppListItemView* CreateAndOpenSingleItemFolder() { |
| // Prepare single folder with a single item in it. |
| AppListFolderItem* folder_item = |
| delegate_->GetTestModel()->CreateSingleItemFolder("single_item_folder", |
| "single"); |
| GetRootGridView()->Layout(); |
| EXPECT_EQ(folder_item, |
| delegate_->GetTestModel()->FindFolderItem("single_item_folder")); |
| EXPECT_EQ(AppListFolderItem::kItemType, folder_item->GetItemType()); |
| |
| EXPECT_EQ(1, GetRootViewModel()->view_size()); |
| AppListItemView* folder_item_view = |
| static_cast<AppListItemView*>(GetRootViewModel()->view_at(0)); |
| EXPECT_EQ(folder_item_view->item(), folder_item); |
| |
| // Click on the folder to open it. |
| EXPECT_FALSE(GetFolderView()->GetVisible()); |
| SimulateClick(folder_item_view); |
| |
| EXPECT_TRUE(GetFolderView()->GetVisible()); |
| |
| return folder_item_view; |
| } |
| |
| AppListItemView* StartDragForReparent(int index_in_folder) { |
| // Start to drag the item in folder. |
| views::View* item_view = GetFolderViewModel()->view_at(index_in_folder); |
| AppListItemView* dragged = SimulateInitiateDrag( |
| GetFolderGridView(), item_view->bounds().CenterPoint()); |
| EXPECT_EQ(item_view, dragged); |
| EXPECT_TRUE(GetRootGridView()->GetVisible()); |
| EXPECT_TRUE(GetFolderView()->GetVisible()); |
| |
| // Drag the item completely outside the folder bounds. |
| gfx::Point drag_target = gfx::Point(-(item_view->width() + 1) / 2, |
| -(item_view->height() + 1) / 2); |
| // 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(GetFolderGridView(), AppsGridView::MOUSE, dragged, |
| drag_target); |
| SimulateUpdateDrag(GetFolderGridView(), AppsGridView::MOUSE, dragged, |
| drag_target); |
| |
| // Fire reparent timer, which should start when the item exits the folder |
| // bounds. The timer closes the folder view. |
| EXPECT_TRUE(GetFolderGridView()->FireFolderItemReparentTimerForTest()); |
| |
| // Note: the folder item is expected to remain visible so it keeps getting |
| // drag events, but it should become completely transparent. |
| EXPECT_TRUE(GetFolderView()->GetVisible()); |
| EXPECT_EQ(0.0f, GetFolderGridView()->layer()->opacity()); |
| return dragged; |
| } |
| |
| void PressKeyInSearchBox(ui::KeyboardCode key_code) { |
| ui::KeyEvent press(ui::ET_KEY_PRESSED, key_code, ui::EF_NONE); |
| search_box_view()->search_box()->OnKeyEvent(&press); |
| } |
| |
| void ClickButton(views::Button* button) { |
| views::test::ButtonTestApi(button).NotifyClick(ui::MouseEvent( |
| ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), base::TimeTicks(), |
| ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON)); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList feature_list_; |
| TestAppListColorProvider app_list_color_provider_; // Needed by AppListView. |
| AshColorProvider ash_color_provider_; // Needed by ContinueContainer. |
| AppListView* app_list_view_ = nullptr; // Owned by native widget. |
| std::unique_ptr<test::AppListTestViewDelegate> delegate_; |
| |
| private: |
| std::unique_ptr<ui::ScopedAnimationDurationScaleMode> zero_duration_mode_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(ProductivityLauncher, |
| AppListMainViewTest, |
| testing::Bool()); |
| |
| // Tests that the close button becomes invisible after close button is clicked. |
| TEST_P(AppListMainViewTest, CloseButtonInvisibleAfterCloseButtonClicked) { |
| PressKeyInSearchBox(ui::VKEY_A); |
| ClickButton(search_box_view()->close_button()); |
| EXPECT_FALSE(search_box_view()->close_button()->GetVisible()); |
| } |
| |
| // Tests that the search box becomes empty after close button is clicked. |
| TEST_P(AppListMainViewTest, SearchBoxEmptyAfterCloseButtonClicked) { |
| PressKeyInSearchBox(ui::VKEY_A); |
| ClickButton(search_box_view()->close_button()); |
| EXPECT_TRUE(search_box_view()->search_box()->GetText().empty()); |
| } |
| |
| // Tests that the search box is no longer active after close button is clicked. |
| TEST_P(AppListMainViewTest, SearchBoxActiveAfterCloseButtonClicked) { |
| PressKeyInSearchBox(ui::VKEY_A); |
| ClickButton(search_box_view()->close_button()); |
| EXPECT_FALSE(search_box_view()->is_search_box_active()); |
| } |
| |
| // Tests changing the AppListModel when switching profiles. |
| TEST_P(AppListMainViewTest, ModelChanged) { |
| delegate_->GetTestModel()->PopulateApps(kInitialItems); |
| EXPECT_EQ(kInitialItems, GetRootViewModel()->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()); |
| std::unique_ptr<SearchModel> old_search_model( |
| delegate_->ReleaseTestSearchModel()); |
| |
| const int kReplacementItems = 5; |
| delegate_->ReplaceTestModel(kReplacementItems); |
| EXPECT_EQ(kReplacementItems, GetRootViewModel()->view_size()); |
| } |
| |
| // 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_P(AppListMainViewTest, DragReparentItemOntoPageSwitcher) { |
| AppListItemView* folder_item_view = CreateAndOpenSingleItemFolder(); |
| ASSERT_TRUE(folder_item_view); |
| |
| // Number of apps to populate. Should provide more than 1 page of apps (5*4 = |
| // 20). |
| const int kNumApps = 30; |
| delegate_->GetTestModel()->PopulateApps(kNumApps); |
| GetRootGridView()->Layout(); |
| |
| EXPECT_EQ(1, GetFolderViewModel()->view_size()); |
| EXPECT_EQ(kNumApps + 1, GetRootViewModel()->view_size()); |
| |
| AppListItemView* dragged = StartDragForReparent(0); |
| |
| // Drag the reparent item to the page switcher. |
| gfx::Point point = GetPageSwitcherView()->GetLocalBounds().CenterPoint(); |
| views::View::ConvertPointToTarget(GetPageSwitcherView(), GetFolderGridView(), |
| &point); |
| SimulateUpdateDrag(GetFolderGridView(), AppsGridView::MOUSE, dragged, point); |
| |
| // Drop it. |
| GetFolderGridView()->EndDrag(false); |
| |
| // The folder should not be destroyed. |
| EXPECT_EQ(kNumApps + 1, GetRootViewModel()->view_size()); |
| AppListFolderItem* const folder_item = |
| delegate_->GetTestModel()->FindFolderItem("single_item_folder"); |
| ASSERT_TRUE(folder_item); |
| EXPECT_EQ(1u, folder_item->item_list()->item_count()); |
| } |
| |
| // 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_P(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(GetRootGridView()->has_dragged_item()); |
| EXPECT_TRUE(GetFolderGridView()->has_dragged_item()); |
| delegate_->GetTestModel()->CreateAndAddItem("Extra"); |
| |
| // The drag operation should get canceled. |
| EXPECT_FALSE(GetRootGridView()->has_dragged_item()); |
| EXPECT_FALSE(GetFolderGridView()->has_dragged_item()); |
| |
| // Additional mouse move operations should be ignored. |
| gfx::Point point(1, 1); |
| SimulateUpdateDrag(GetFolderGridView(), AppsGridView::MOUSE, dragged, point); |
| EXPECT_FALSE(GetRootGridView()->has_dragged_item()); |
| EXPECT_FALSE(GetFolderGridView()->has_dragged_item()); |
| } |
| |
| // 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_P(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(GetRootGridView(), GetFolderGridView(), |
| &drag_point); |
| |
| AppListItemView* dragged = StartDragForReparent(0); |
| |
| // Drag the reparent item back into its folder. |
| SimulateUpdateDrag(GetFolderGridView(), AppsGridView::MOUSE, dragged, |
| drag_point); |
| GetFolderGridView()->EndDrag(false); |
| |
| // The app list model should remain unchanged. |
| EXPECT_EQ(2, GetRootViewModel()->view_size()); |
| EXPECT_EQ(folder_id, GetRootGridView()->GetItemViewAt(0)->item()->id()); |
| AppListFolderItem* const folder_item = |
| delegate_->GetTestModel()->FindFolderItem("single_item_folder"); |
| ASSERT_TRUE(folder_item); |
| EXPECT_EQ(1u, folder_item->item_list()->item_count()); |
| } |
| |
| } // namespace ash |