chromium/ash/app_list/views/scrollable_apps_grid_view_unittest.cc

// Copyright 2021 The Chromium Authors
// 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/scrollable_apps_grid_view.h"

#include <cstddef>
#include <limits>
#include <list>
#include <memory>
#include <string>
#include <utility>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_item_list.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/model/app_list_test_model.h"
#include "ash/app_list/test/app_list_test_helper.h"
#include "ash/app_list/test_app_list_client.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/apps_grid_view_test_api.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/shelf_item_delegate.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/test/test_shelf_item_delegate.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {

class ShelfItemFactoryFake : public ShelfModel::ShelfItemFactory {
 public:
  virtual ~ShelfItemFactoryFake() = default;

  // ShelfModel::ShelfItemFactory:
  std::unique_ptr<ShelfItem> CreateShelfItemForApp(
      const ShelfID& shelf_id,
      ShelfItemStatus status,
      ShelfItemType shelf_item_type,
      const std::u16string& title) override {
    auto item = std::make_unique<ShelfItem>();
    item->id = shelf_id;
    item->status = status;
    item->type = shelf_item_type;
    item->title = title;
    return item;
  }

  std::unique_ptr<ShelfItemDelegate> CreateShelfItemDelegateForAppId(
      const std::string& app_id) override {
    return std::make_unique<TestShelfItemDelegate>(ShelfID(app_id));
  }
};

}  // namespace

class ScrollableAppsGridViewTest : public AshTestBase,
                                   public testing::WithParamInterface<bool> {
 public:
  ScrollableAppsGridViewTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ~ScrollableAppsGridViewTest() override = default;

  void SetUp() override {
    scoped_feature_list_.InitWithFeatureState(
        app_list_features::kDragAndDropRefactor, GetParam());
    AshTestBase::SetUp();

    shelf_item_factory_ = std::make_unique<ShelfItemFactoryFake>();
    ShelfModel::Get()->SetShelfItemFactory(shelf_item_factory_.get());
  }

  void TearDown() override {
    ShelfModel::Get()->SetShelfItemFactory(nullptr);
    AshTestBase::TearDown();
  }

  test::AppListTestModel::AppListTestItem* AddAppListItem(
      const std::string& id) {
    return GetAppListTestHelper()->model()->CreateAndAddItem(id);
  }

  void PopulateApps(int n) { GetAppListTestHelper()->model()->PopulateApps(n); }

  void DeleteApps(int n) {
    AppListItemList* item_list =
        GetAppListTestHelper()->model()->top_level_item_list();
    for (int i = 0; i < n; i++) {
      GetAppListTestHelper()->model()->DeleteItem(item_list->item_at(0)->id());
    }
  }

  AppListFolderItem* CreateAndPopulateFolderWithApps(int n) {
    return GetAppListTestHelper()->model()->CreateAndPopulateFolderWithApps(n);
  }

  void SimulateKeyPress(ui::KeyboardCode key_code, int flags = ui::EF_NONE) {
    GetEventGenerator()->PressKey(key_code, flags);
  }

  void SimulateKeyReleased(ui::KeyboardCode key_code, int flags = ui::EF_NONE) {
    GetEventGenerator()->ReleaseKey(key_code, flags);
  }

  void ShowAppList() {
    GetAppListTestHelper()->ShowAppList();

    apps_grid_view_ = GetAppListTestHelper()->GetScrollableAppsGridView();
    scroll_view_ = apps_grid_view_->scroll_view_for_test();
  }

  AppListItemView* StartDragOnView(AppListItemView* item) {
    auto* generator = GetEventGenerator();
    generator->MoveMouseTo(item->GetBoundsInScreen().CenterPoint());
    generator->PressLeftButton();
    EXPECT_TRUE(item->FireMouseDragTimerForTest());
    return item;
  }

  AppListItemView* StartDragOnItemViewAt(int item_index) {
    return StartDragOnView(apps_grid_view_->GetItemViewAt(item_index));
  }

  AppListItemView* StartDragOnItemInFolderAt(int item_index) {
    DCHECK(GetAppListTestHelper()->IsInFolderView());
    auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
    AppListItemView* item =
        folder_view->items_grid_view()->GetItemViewAt(item_index);
    return StartDragOnView(item);
  }

  void DragItemOutOfFolder() {
    ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());
    auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
    ASSERT_TRUE(folder_view->items_grid_view()->has_dragged_item());
    gfx::Point outside_view = folder_view->GetBoundsInScreen().bottom_right();
    GetEventGenerator()->MoveMouseTo(outside_view);
    GetEventGenerator()->MoveMouseBy(10, 10);
    folder_view->items_grid_view()->FireFolderItemReparentTimerForTest();
  }

  ScrollableAppsGridView* GetScrollableAppsGridView() {
    return GetAppListTestHelper()->GetScrollableAppsGridView();
  }

  // Verifies the visible item index range.
  bool IsIndexRangeExpected(size_t first_index, size_t last_index) {
    const std::optional<AppsGridView::VisibleItemIndexRange> index_range =
        apps_grid_view_->GetVisibleItemIndexRange();

    return index_range->first_index == first_index &&
           index_range->last_index == last_index;
  }

  std::unique_ptr<ShelfItemFactoryFake> shelf_item_factory_;

  // Cache some view pointers to make the tests more concise.
  raw_ptr<ScrollableAppsGridView, DanglingUntriaged> apps_grid_view_ = nullptr;
  raw_ptr<views::ScrollView, DanglingUntriaged> scroll_view_ = nullptr;
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All, ScrollableAppsGridViewTest, testing::Bool());

TEST_P(ScrollableAppsGridViewTest, ClickOnApp) {
  AddAppListItem("id");

  ShowAppList();

  // Click on the first icon.
  ScrollableAppsGridView* view = GetScrollableAppsGridView();
  views::View* icon = view->GetItemViewAt(0);
  GetEventGenerator()->MoveMouseTo(icon->GetBoundsInScreen().CenterPoint());
  GetEventGenerator()->ClickLeftButton();

  // The item was activated.
  EXPECT_EQ(1, GetTestAppListClient()->activate_item_count());
  EXPECT_EQ("id", GetTestAppListClient()->activate_item_last_id());
}

TEST_P(ScrollableAppsGridViewTest, DragApp) {
  base::HistogramTester histogram_tester;
  AddAppListItem("id1");
  AddAppListItem("id2");
  ShowAppList();

  // Start dragging the first item.
  StartDragOnItemViewAt(0);

  auto* generator = GetEventGenerator();
  // Drag to the right of the second item.
  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    gfx::Size tile_size = apps_grid_view_->GetTotalTileSize(/*page=*/0);
    generator->MoveMouseBy(tile_size.width() * 2, 0);
  }));
  tasks.push_back(base::BindLambdaForTesting(
      [&]() { GetEventGenerator()->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  generator->ReleaseLeftButton();

  // The item was not activated.
  EXPECT_EQ(0, GetTestAppListClient()->activate_item_count());

  // Items were reordered.
  AppListItemList* item_list =
      GetAppListTestHelper()->model()->top_level_item_list();
  ASSERT_EQ(2u, item_list->item_count());
  EXPECT_EQ("id2", item_list->item_at(0)->id());
  EXPECT_EQ("id1", item_list->item_at(1)->id());

  // Reordering apps is recorded in the histogram tester.
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByDragInTopLevel, 1);
}

TEST_P(ScrollableAppsGridViewTest, SearchBoxHasFocusAfterDrag) {
  PopulateApps(2);
  ShowAppList();

  // Drag the first item to the right.
  AppListItemView* item = StartDragOnItemViewAt(0);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting(
      [&]() { GetEventGenerator()->MoveMouseBy(250, 0); }));
  tasks.push_back(base::BindLambdaForTesting(
      [&]() { GetEventGenerator()->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The item does not have focus, but it might be selected.
  EXPECT_FALSE(item->HasFocus());

  // The search box has focus.
  auto* search_box_view = GetAppListTestHelper()->GetBubbleSearchBoxView();
  EXPECT_TRUE(search_box_view->search_box()->HasFocus());
  EXPECT_TRUE(search_box_view->is_search_box_active());
}

TEST_P(ScrollableAppsGridViewTest, DragAppAfterScrollingDown) {
  // Simulate data from another device.
  PopulateApps(20);
  AddAppListItem("aaa");
  AddAppListItem("bbb");
  ShowAppList();

  // "aaa" and "bbb" are the last two items.
  AppListItemList* item_list =
      GetAppListTestHelper()->model()->top_level_item_list();
  ASSERT_EQ(22u, item_list->item_count());
  ASSERT_EQ("aaa", item_list->item_at(20)->id());
  ASSERT_EQ("bbb", item_list->item_at(21)->id());

  // Scroll down to the "aaa" item.
  auto* apps_grid_view = GetScrollableAppsGridView();
  AppListItemView* item = apps_grid_view->GetItemViewAt(20);
  ASSERT_EQ("aaa", item->item()->id());
  item->ScrollViewToVisible();

  auto* generator = GetEventGenerator();
  // Drag the "aaa" item to the right.
  StartDragOnView(item);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    gfx::Size tile_size = apps_grid_view->GetTotalTileSize(/*page=*/0);
    generator->MoveMouseBy(tile_size.width() * 2, 0);
  }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The last 2 items were reordered.
  EXPECT_EQ("bbb", item_list->item_at(20)->id()) << item_list->ToString();
  EXPECT_EQ("aaa", item_list->item_at(21)->id()) << item_list->ToString();
}

TEST_P(ScrollableAppsGridViewTest, AutoScrollDown) {
  PopulateApps(30);
  ShowAppList();

  // Scroll view starts at the top.
  const int initial_scroll_offset = scroll_view_->GetVisibleRect().y();
  EXPECT_EQ(initial_scroll_offset, 0);

  // Drag an item into the bottom auto-scroll margin.
  StartDragOnItemViewAt(0);

  auto* generator = GetEventGenerator();
  int scroll_offset_start, scroll_offset_end;
  // Drag an item into the bottom auto-scroll margin.
  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(scroll_view_->GetBoundsInScreen().bottom_center() +
                           gfx::Vector2d(0, -5));
    // The scroll view scrolls immediately.
    scroll_offset_start = scroll_view_->GetVisibleRect().y();
    EXPECT_GT(scroll_offset_start, 0);
    // Scroll timer is running.
    EXPECT_TRUE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    // Reordering is paused.
    EXPECT_FALSE(apps_grid_view_->reorder_timer_for_test()->IsRunning());
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Holding the mouse in place for a while scrolls down more.
    task_environment()->FastForwardBy(base::Milliseconds(100));
    scroll_offset_end = scroll_view_->GetVisibleRect().y();
    EXPECT_GT(scroll_offset_end, scroll_offset_start);
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Move the mouse back into the (vertical) center of the view (not in the
    // scroll margin). Use a point within the grid and not the scroll view,
    // since the apps grid is the target for drop events.
    generator->MoveMouseTo(apps_grid_view_->GetBoundsInScreen().left_center());
    // Scroll position didn't change, auto-scrolling is stopped, and reordering
    // started again.
    EXPECT_EQ(scroll_offset_end, scroll_view_->GetVisibleRect().y());
    EXPECT_FALSE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    EXPECT_TRUE(apps_grid_view_->reorder_timer_for_test()->IsRunning());
    GetEventGenerator()->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

TEST_P(ScrollableAppsGridViewTest, DoesNotAutoScrollUpWhenAtTop) {
  PopulateApps(30);
  ShowAppList();

  auto* generator = GetEventGenerator();
  // Drag an item into the top auto-scroll margin and wait a while.
  StartDragOnItemViewAt(0);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(apps_grid_view_->GetBoundsInScreen().top_center());
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Milliseconds(500));
    // View did not scroll.
    int scroll_offset = scroll_view_->GetVisibleRect().y();
    EXPECT_EQ(scroll_offset, 0);
    EXPECT_FALSE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

TEST_P(ScrollableAppsGridViewTest, DoesNotAutoScrollDownWhenAtBottom) {
  PopulateApps(30);
  ShowAppList();

  // Scroll the view to the bottom.
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
                                 std::numeric_limits<int>::max());
  int initial_scroll_offset = scroll_view_->GetVisibleRect().y();

  // Drag an item into the bottom auto-scroll margin and wait a while.
  StartDragOnItemViewAt(29);

  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(
        apps_grid_view_->GetBoundsInScreen().bottom_center());
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Milliseconds(500));
    // View did not scroll.
    int scroll_offset = scroll_view_->GetVisibleRect().y();
    EXPECT_EQ(scroll_offset, initial_scroll_offset);
    EXPECT_FALSE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

TEST_P(ScrollableAppsGridViewTest, DoesNotAutoScrollWhenDraggedToTheRight) {
  PopulateApps(30);
  ShowAppList();

  // Drag an item outside the bottom-right corner of the scroll view (i.e.
  // towards the shelf).
  StartDragOnItemViewAt(0);

  auto* generator = GetEventGenerator();
  gfx::Point point = apps_grid_view_->GetBoundsInScreen().bottom_right();
  point.Offset(10, 10);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->MoveMouseTo(point); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Milliseconds(500));
    // View did not scroll.
    int scroll_offset = scroll_view_->GetVisibleRect().y();
    EXPECT_EQ(scroll_offset, 0);
    EXPECT_FALSE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

TEST_P(ScrollableAppsGridViewTest, DoesNotAutoScrollWhenAboveWidget) {
  PopulateApps(30);
  ShowAppList();

  // Scroll the view to the bottom.
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
                                 std::numeric_limits<int>::max());
  int initial_scroll_offset = scroll_view_->GetVisibleRect().y();

  // Drag an item above the widget scroll margin.
  StartDragOnItemViewAt(29);

  auto* generator = GetEventGenerator();
  gfx::Point point =
      scroll_view_->GetWidget()->GetWindowBoundsInScreen().top_center();
  point.Offset(0, -10);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->MoveMouseTo(point); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Milliseconds(500));
    // View did not scroll.
    int scroll_offset = scroll_view_->GetVisibleRect().y();
    EXPECT_EQ(scroll_offset, initial_scroll_offset);
    EXPECT_FALSE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

TEST_P(ScrollableAppsGridViewTest, DoesNotAutoScrollWhenBelowWidget) {
  PopulateApps(30);
  ShowAppList();

  // Drag an item below the widget scroll margin.
  StartDragOnItemViewAt(0);

  auto* generator = GetEventGenerator();
  gfx::Point point =
      scroll_view_->GetWidget()->GetWindowBoundsInScreen().bottom_center();
  point.Offset(0, 10);

  std::list<base::OnceClosure> tasks;
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->MoveMouseTo(point); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    task_environment()->FastForwardBy(base::Milliseconds(500));
    // View did not scroll.
    int scroll_offset = scroll_view_->GetVisibleRect().y();
    EXPECT_EQ(scroll_offset, 0);
    EXPECT_FALSE(apps_grid_view_->auto_scroll_timer_for_test()->IsRunning());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);
}

// Regression test for https://crbug.com/1258954
TEST_P(ScrollableAppsGridViewTest, DragItemIntoEmptySpaceWillReorderToEnd) {
  AddAppListItem("id1");
  AddAppListItem("id2");
  AddAppListItem("id3");
  ShowAppList();

  // The grid view is taller than the single row of apps, so it can handle drops
  // in the empty region.
  EXPECT_GT(apps_grid_view_->height(),
            apps_grid_view_->GetTileGridSize().height());

  // Drag and drop the first item straight down below the first row.
  StartDragOnItemViewAt(0);

  gfx::Size tile_size = apps_grid_view_->GetTotalTileSize(/*page=*/0);
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseBy(0, tile_size.height());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The first item was reordered to the end.
  AppListItemList* item_list =
      GetAppListTestHelper()->model()->top_level_item_list();
  ASSERT_EQ(3u, item_list->item_count());
  EXPECT_EQ("id2", item_list->item_at(0)->id());
  EXPECT_EQ("id3", item_list->item_at(1)->id());
  EXPECT_EQ("id1", item_list->item_at(2)->id());
}

TEST_P(ScrollableAppsGridViewTest, ChangingAppListModelUpdatesAppsGridHeight) {
  // Start with 4 rows of 5.
  PopulateApps(20);
  ShowAppList();

  // Adding one row of 5 causes the grid size to expand.
  const int height_before_adding = apps_grid_view_->height();
  PopulateApps(5);
  apps_grid_view_->GetWidget()->LayoutRootViewIfNecessary();
  EXPECT_GT(apps_grid_view_->height(), height_before_adding);

  // Removing one row of 5 causes the grid size to contract.
  const int height_before_removing = apps_grid_view_->height();
  DeleteApps(5);
  apps_grid_view_->GetWidget()->LayoutRootViewIfNecessary();
  EXPECT_LT(apps_grid_view_->height(), height_before_removing);
}

TEST_P(ScrollableAppsGridViewTest, SmallFolderHasCorrectWidth) {
  CreateAndPopulateFolderWithApps(2);
  ShowAppList();

  // Enter the folder view.
  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  auto* folder_view = GetAppListTestHelper()->GetBubbleFolderView();
  auto* items_grid_view = folder_view->items_grid_view();
  const int tile_width = items_grid_view->app_list_config()->grid_tile_width();

  // Spec calls for 8 dips of padding at edges and between tiles.
  EXPECT_EQ(folder_view->width(), 8 + tile_width + 8 + tile_width + 8);

  // The leftmost item is flush to the left of the grid.
  EXPECT_EQ(items_grid_view->GetItemViewAt(0)->bounds().x(), 0);

  // The rightmost item is flush to the right of the grid.
  EXPECT_EQ(items_grid_view->GetItemViewAt(1)->bounds().right(),
            items_grid_view->GetLocalBounds().right());
}

TEST_P(ScrollableAppsGridViewTest, DragItemToReorderInFolderRecordsHistogram) {
  base::HistogramTester histogram_tester;
  // Create a folder with 3 apps.
  AppListFolderItem* folder_item = CreateAndPopulateFolderWithApps(3);
  ShowAppList();

  // Enter the folder view.
  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // Drag the first app in the folder.
  AppListItemView* item_view = StartDragOnItemInFolderAt(0);

  // Drag the item to the third position in the folder.
  gfx::Size tile_size = apps_grid_view_->GetTotalTileSize(/*page=*/0);
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseBy(0, tile_size.height());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The item is now reordered in the folder and the reordering is recorded.
  EXPECT_EQ(3u, folder_item->ChildItemCount());
  EXPECT_EQ(folder_item->item_list()->item_at(2)->id(),
            item_view->item()->id());
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByDragInFolder, 1);
}

TEST_P(ScrollableAppsGridViewTest, DragItemIntoFolderRecordsHistogram) {
  base::HistogramTester histogram_tester;
  // Create a folder and an app.
  AppListFolderItem* folder_item = CreateAndPopulateFolderWithApps(3);
  AddAppListItem("dragged_item");
  ShowAppList();

  // Drag the app in the top level app list into the folder.
  StartDragOnItemViewAt(1);
  ASSERT_TRUE(apps_grid_view_->GetItemViewAt(0)->item()->is_folder());
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    generator->MoveMouseTo(
        apps_grid_view_->GetItemViewAt(0)->GetBoundsInScreen().CenterPoint());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The dragged app is now in the folder and the reordering is recorded.
  EXPECT_EQ(4u, folder_item->ChildItemCount());
  EXPECT_EQ(folder_item->item_list()->item_at(3)->id(), "dragged_item");
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveByDragIntoFolder, 1);
}

TEST_P(ScrollableAppsGridViewTest, DragItemOutOfFolderRecordsHistogram) {
  base::HistogramTester histogram_tester;
  // Create a folder with 3 apps.
  AppListFolderItem* folder_item = CreateAndPopulateFolderWithApps(3);
  ShowAppList();

  // Enter the folder view.
  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // Drag the first app in the folder and move it out of the folder.
  AppListItemView* item_view = StartDragOnItemInFolderAt(0);
  std::string item_id = item_view->item()->id();
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() { DragItemOutOfFolder(); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Drag the app item to near the expected end position and end the drag.
    generator->MoveMouseTo(
        apps_grid_view_->GetItemViewAt(0)->GetBoundsInScreen().right_center() +
        gfx::Vector2d(20, 0));
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // The folder view should be closed and invisible after releasing the drag.
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  // The dragged item is now in the top level item list and the reordering is
  // recorded.
  AppListItemList* item_list =
      GetAppListTestHelper()->model()->top_level_item_list();
  EXPECT_EQ(2u, item_list->item_count());
  EXPECT_EQ(item_list->item_at(1)->id(), item_id);
  EXPECT_EQ(2u, folder_item->item_list()->item_count());
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveByDragOutOfFolder, 1);
}

TEST_P(ScrollableAppsGridViewTest,
       DragItemFromOneFolderToAnotherRecordsHistogram) {
  base::HistogramTester histogram_tester;
  // Create two folders.
  AppListFolderItem* folder_item_1 = CreateAndPopulateFolderWithApps(3);
  AppListFolderItem* folder_item_2 = CreateAndPopulateFolderWithApps(2);
  ShowAppList();

  // Enter the view of the first folder.
  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // Drag the first app in the folder and move it out of the folder.
  StartDragOnItemInFolderAt(0);
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() { DragItemOutOfFolder(); }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Move the app item into the other folder and end the drag.
    generator->MoveMouseTo(
        apps_grid_view_->GetItemViewAt(1)->GetBoundsInScreen().CenterPoint());
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  // No folder view is showing now.
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
  EXPECT_FALSE(GetAppListTestHelper()->GetBubbleFolderView()->GetVisible());

  // The dragged item was moved to another folder and the reordering is
  // recorded.
  EXPECT_EQ(2u, folder_item_1->item_list()->item_count());
  EXPECT_EQ(3u, folder_item_2->item_list()->item_count());
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveIntoAnotherFolder, 1);
}

TEST_P(ScrollableAppsGridViewTest, ReparentDragToNewRow) {
  const int kInitialItems = 20;
  AppListFolderItem* folder_item = CreateAndPopulateFolderWithApps(3);
  PopulateApps(kInitialItems - 1);
  ShowAppList();

  ASSERT_EQ(
      0u, apps_grid_view_->view_model()->view_size() % apps_grid_view_->cols());

  // Enter the view of the first folder.
  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // Drag the first app in the folder and move it out of the folder.
  AppListItemView* const dragged_view = StartDragOnItemInFolderAt(0);
  const std::string dragged_item_id = dragged_view->item()->id();
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    DragItemOutOfFolder();
    ASSERT_TRUE(apps_grid_view_->reorder_timer_for_test()->IsRunning());
    apps_grid_view_->reorder_timer_for_test()->FireNow();
    apps_grid_view_->GetWidget()->LayoutRootViewIfNecessary();
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Scroll the grid to the bottom.
    apps_grid_view_->ScrollRectToVisible(gfx::Rect(
        apps_grid_view_->GetLocalBounds().bottom_center() - gfx::Vector2d(0, 1),
        gfx::Size(1, 1)));
    // Drop the item over expected first empty slot bounds. This should drop the
    // item into the last slot.
    gfx::Rect last_slot_bounds =
        test::AppsGridViewTestApi(apps_grid_view_)
            .GetItemTileRectOnCurrentPageAt(
                kInitialItems / apps_grid_view_->cols(), 1);
    views::View::ConvertRectToScreen(apps_grid_view_, &last_slot_bounds);
    generator->MoveMouseTo(last_slot_bounds.CenterPoint());
    ASSERT_TRUE(apps_grid_view_->reorder_timer_for_test()->IsRunning());
    apps_grid_view_->reorder_timer_for_test()->FireNow();
  }));
  tasks.push_back(
      base::BindLambdaForTesting([&]() { generator->ReleaseLeftButton(); }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  AppListItemView* last_item = apps_grid_view_->GetItemViewAt(kInitialItems);
  ASSERT_TRUE(last_item);
  EXPECT_EQ(dragged_item_id, last_item->item()->id());
  EXPECT_EQ("", last_item->item()->folder_id());
  EXPECT_EQ(2u, folder_item->ChildItemCount());
}

TEST_P(ScrollableAppsGridViewTest, CanceledReparentDragToNewRow) {
  const int kInitialItems = 20;
  AppListFolderItem* folder_item = CreateAndPopulateFolderWithApps(3);
  PopulateApps(kInitialItems - 1);
  ShowAppList();

  ASSERT_EQ(
      0u, apps_grid_view_->view_model()->view_size() % apps_grid_view_->cols());

  const gfx::Size initial_preferred_size = apps_grid_view_->GetPreferredSize();

  // Enter the view of the first folder.
  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // Drag the first app in the folder and move it out of the folder.
  AppListItemView* const dragged_view = StartDragOnItemInFolderAt(0);
  const std::string dragged_item_id = dragged_view->item()->id();
  auto* generator = GetEventGenerator();

  std::list<base::OnceClosure> tasks;
  tasks.push_back(base::BindLambdaForTesting([&]() {
    DragItemOutOfFolder();
    ASSERT_TRUE(apps_grid_view_->reorder_timer_for_test()->IsRunning());
    apps_grid_view_->reorder_timer_for_test()->FireNow();
    apps_grid_view_->GetWidget()->LayoutRootViewIfNecessary();
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Scroll the grid to the bottom.
    apps_grid_view_->ScrollRectToVisible(gfx::Rect(
        apps_grid_view_->GetLocalBounds().bottom_center() - gfx::Vector2d(0, 1),
        gfx::Size(1, 1)));
  }));
  tasks.push_back(base::BindLambdaForTesting([&]() {
    // Move the mouse pointer outside the apps grid bounds, and release it. This
    // should cancel the reparent drag operation.
    generator->MoveMouseTo(
        apps_grid_view_->GetBoundsInScreen().bottom_center() +
        gfx::Vector2d(0, 50));
    generator->ReleaseLeftButton();
  }));
  MaybeRunDragAndDropSequenceForAppList(&tasks, /*is_touch=*/false);

  EXPECT_EQ(initial_preferred_size, apps_grid_view_->GetPreferredSize());
  AppListItemView* last_item = apps_grid_view_->GetItemViewAt(kInitialItems);
  EXPECT_FALSE(last_item);
  AppListItem* dragged_item =
      GetAppListTestHelper()->model()->FindItem(dragged_item_id);
  ASSERT_TRUE(dragged_item);
  EXPECT_EQ(folder_item->id(), dragged_item->folder_id());
  EXPECT_EQ(3u, folder_item->ChildItemCount());
}

TEST_P(ScrollableAppsGridViewTest, LeftAndRightArrowKeysMoveSelection) {
  PopulateApps(2);
  ShowAppList();

  auto* apps_grid_view = GetScrollableAppsGridView();
  AppListItemView* item1 = apps_grid_view->GetItemViewAt(0);
  AppListItemView* item2 = apps_grid_view->GetItemViewAt(1);

  apps_grid_view->GetFocusManager()->SetFocusedView(item1);
  EXPECT_TRUE(item1->HasFocus());

  PressAndReleaseKey(ui::VKEY_RIGHT);
  EXPECT_FALSE(item1->HasFocus());
  EXPECT_TRUE(item2->HasFocus());

  PressAndReleaseKey(ui::VKEY_LEFT);
  EXPECT_TRUE(item1->HasFocus());
  EXPECT_FALSE(item2->HasFocus());
}

TEST_P(ScrollableAppsGridViewTest, ArrowKeysCanMoveFocusOutOfGrid) {
  PopulateApps(2);
  ShowAppList();

  auto* apps_grid_view = GetScrollableAppsGridView();
  AppListItemView* item1 = apps_grid_view->GetItemViewAt(0);
  AppListItemView* item2 = apps_grid_view->GetItemViewAt(1);

  // Moving left from the first item removes focus from the grid.
  apps_grid_view->GetFocusManager()->SetFocusedView(item1);
  PressAndReleaseKey(ui::VKEY_LEFT);
  EXPECT_FALSE(item1->HasFocus());
  EXPECT_FALSE(item2->HasFocus());

  // Moving up from the first item removes focus from the grid.
  apps_grid_view->GetFocusManager()->SetFocusedView(item1);
  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_FALSE(item1->HasFocus());
  EXPECT_FALSE(item2->HasFocus());

  // Moving right from the last item removes focus from the grid.
  apps_grid_view->GetFocusManager()->SetFocusedView(item2);
  PressAndReleaseKey(ui::VKEY_RIGHT);
  EXPECT_FALSE(item1->HasFocus());
  EXPECT_FALSE(item2->HasFocus());

  // Moving down from the last item removes focus from the grid.
  apps_grid_view->GetFocusManager()->SetFocusedView(item2);
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_FALSE(item1->HasFocus());
  EXPECT_FALSE(item2->HasFocus());
}

// Tests that histograms are recorded when apps are moved with control+arrow.
TEST_P(ScrollableAppsGridViewTest, ControlArrowRecordsHistogramBasic) {
  base::HistogramTester histogram_tester;
  PopulateApps(20);
  ShowAppList();
  ScrollableAppsGridView* apps_grid_view = GetScrollableAppsGridView();

  AppListItemView* moving_item = apps_grid_view->GetItemViewAt(0);
  apps_grid_view->GetFocusManager()->SetFocusedView(moving_item);

  // Make one move right and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_RIGHT, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInTopLevel, 1);

  // Make one move down and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_DOWN, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_DOWN, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInTopLevel, 2);

  // Make one move up and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_UP, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_UP, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInTopLevel, 3);

  // Make one move left and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_LEFT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_LEFT, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInTopLevel, 4);
}

// Test that histograms do not record when the keyboard move is a no-op.
TEST_P(ScrollableAppsGridViewTest,
       ControlArrowDoesNotRecordHistogramWithNoOpMove) {
  base::HistogramTester histogram_tester;
  PopulateApps(20);
  ShowAppList();
  ScrollableAppsGridView* apps_grid_view = GetScrollableAppsGridView();

  AppListItemView* moving_item = apps_grid_view->GetItemViewAt(0);
  apps_grid_view->GetFocusManager()->SetFocusedView(moving_item);

  // Make 2 no-op moves and one successful move from 0,0 and expect a histogram
  // is recorded only once.
  SimulateKeyPress(ui::VKEY_LEFT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_LEFT, ui::EF_NONE);

  SimulateKeyPress(ui::VKEY_UP, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_UP, ui::EF_NONE);

  SimulateKeyPress(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_RIGHT, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInTopLevel, 1);
}

// Tests that histograms are recorded in folder view when apps are moved with
// control+arrow.
TEST_P(ScrollableAppsGridViewTest, ControlArrowRecordsHistogramInFolderBasic) {
  base::HistogramTester histogram_tester;
  CreateAndPopulateFolderWithApps(4);
  ShowAppList();
  ScrollableAppsGridView* apps_grid_view = GetScrollableAppsGridView();

  // Select the folder item in the grid.
  AppListItemView* folder_item_view = apps_grid_view->GetItemViewAt(0);
  EXPECT_TRUE(folder_item_view->item()->is_folder());

  // Enter the folder view.
  apps_grid_view->GetFocusManager()->SetFocusedView(folder_item_view);
  SimulateKeyPress(ui::VKEY_RETURN);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // If the folder view is entered by pressing return key while the focus is on
  // the folder, the focus will move to the first item inside the folder view.
  AppsGridView* folder_grid_view =
      GetAppListTestHelper()->GetBubbleFolderView()->items_grid_view();
  EXPECT_EQ(apps_grid_view->GetFocusManager()->GetFocusedView(),
            folder_grid_view->GetItemViewAt(0));

  // Make one move right and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_RIGHT, ui::EF_NONE);
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInFolder, 1);

  // Make one move down and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_DOWN, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_DOWN, ui::EF_NONE);
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInFolder, 2);

  // Make one move left and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_LEFT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_LEFT, ui::EF_NONE);
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInFolder, 3);

  // Make one move up and expect a histogram is recorded.
  SimulateKeyPress(ui::VKEY_UP, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_UP, ui::EF_NONE);
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInFolder, 4);
}

// Tests that histograms do not record when the keyboard move is a no-op in the
// folder view.
TEST_P(ScrollableAppsGridViewTest,
       ControlArrowDoesNotRecordHistogramWithNoOpMoveInFolder) {
  base::HistogramTester histogram_tester;
  CreateAndPopulateFolderWithApps(4);
  ShowAppList();
  ScrollableAppsGridView* apps_grid_view = GetScrollableAppsGridView();

  // Select the folder item in the grid.
  AppListItemView* folder_item_view = apps_grid_view->GetItemViewAt(0);
  EXPECT_TRUE(folder_item_view->item()->is_folder());

  // Enter the folder view.
  apps_grid_view->GetFocusManager()->SetFocusedView(folder_item_view);
  SimulateKeyPress(ui::VKEY_RETURN);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());

  // If the folder view is entered by pressing return key while the focus is on
  // the folder, the focus will move to the first item inside the folder view.
  AppsGridView* folder_grid_view =
      GetAppListTestHelper()->GetBubbleFolderView()->items_grid_view();
  EXPECT_EQ(apps_grid_view->GetFocusManager()->GetFocusedView(),
            folder_grid_view->GetItemViewAt(0));

  // Make 2 no-op moves and one successful move from 0,0 and expect a histogram
  // is recorded only once.
  SimulateKeyPress(ui::VKEY_LEFT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_LEFT, ui::EF_NONE);

  SimulateKeyPress(ui::VKEY_UP, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_UP, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInFolder, 0);

  SimulateKeyPress(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN);
  SimulateKeyReleased(ui::VKEY_RIGHT, ui::EF_NONE);

  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kReorderByKeyboardInFolder, 1);
}

// Tests that control + shift + arrow moves selected item out of a folder.
TEST_P(ScrollableAppsGridViewTest, ControlShiftArrowMovesItemOutOfFolder) {
  base::HistogramTester histogram_tester;
  AppListFolderItem* folder_item = CreateAndPopulateFolderWithApps(5);
  ShowAppList();
  ScrollableAppsGridView* apps_grid_view = GetScrollableAppsGridView();

  // Select the folder item in the grid.
  AppListItemView* folder_item_view = apps_grid_view->GetItemViewAt(0);
  EXPECT_TRUE(folder_item_view->item()->is_folder());

  // Enter the folder view and move the item out of and to the left of the
  // folder.
  apps_grid_view->GetFocusManager()->SetFocusedView(folder_item_view);
  SimulateKeyPress(ui::VKEY_RETURN);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());
  SimulateKeyPress(ui::VKEY_LEFT, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(apps_grid_view->selected_view(), apps_grid_view->GetItemViewAt(0));
  EXPECT_EQ(4u, folder_item->ChildItemCount());
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveByKeyboardOutOfFolder, 1);

  // Enter the folder view and move the item out of and to the right of the
  // folder.
  apps_grid_view->GetFocusManager()->SetFocusedView(folder_item_view);
  SimulateKeyPress(ui::VKEY_RETURN);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());
  SimulateKeyPress(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(apps_grid_view->selected_view(), apps_grid_view->GetItemViewAt(2));
  EXPECT_EQ(3u, folder_item->ChildItemCount());
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveByKeyboardOutOfFolder, 2);

  // Enter the folder view and move the item out of and to the above of the
  // folder.
  apps_grid_view->GetFocusManager()->SetFocusedView(folder_item_view);
  SimulateKeyPress(ui::VKEY_RETURN);
  SimulateKeyPress(ui::VKEY_UP, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(apps_grid_view->selected_view(), apps_grid_view->GetItemViewAt(1));
  EXPECT_EQ(2u, folder_item->ChildItemCount());
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveByKeyboardOutOfFolder, 3);

  // Enter the folder view and move the item out of and to the below of the
  // folder.
  apps_grid_view->GetFocusManager()->SetFocusedView(folder_item_view);
  SimulateKeyPress(ui::VKEY_RETURN);
  ASSERT_TRUE(GetAppListTestHelper()->IsInFolderView());
  SimulateKeyPress(ui::VKEY_DOWN, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(apps_grid_view->selected_view(), apps_grid_view->GetItemViewAt(3));
  histogram_tester.ExpectBucketCount("Apps.AppListBubbleAppMovingType",
                                     kMoveByKeyboardOutOfFolder, 4);
  EXPECT_FALSE(GetAppListTestHelper()->IsInFolderView());
}

// Verify on the apps grid with zero scroll offset.
TEST_P(ScrollableAppsGridViewTest, VerifyVisibleRangeByDefault) {
  PopulateApps(33);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so choose a number that is not the
  // multiple of 5 as the total item count.
  ASSERT_EQ(5, cols);

  // Verify that the items on row 0 to row 3 are visible at default.
  EXPECT_TRUE(
      IsIndexRangeExpected(/*first_index=*/0, /*last_index=*/4 * cols - 1));
}

// Verify on the apps grid whose first row is unfilled.
TEST_P(ScrollableAppsGridViewTest, VerifyVisibleRangeFirstRowUnfilled) {
  PopulateApps(4);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so choose a smaller number as the total
  // item count.
  ASSERT_EQ(5, cols);

  // Verify that the items on the first row are visible at default.
  EXPECT_TRUE(IsIndexRangeExpected(/*first_index=*/0, /*last_index=*/3));
}

// Verify on the apps grid whose first row is filled.
TEST_P(ScrollableAppsGridViewTest, VerifyVisibleRangeFirstRowFilled) {
  PopulateApps(5);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so apps just fill the first row.
  ASSERT_EQ(5, cols);

  // Verify that the items on the first row are visible at default.
  EXPECT_TRUE(IsIndexRangeExpected(/*first_index=*/0, /*last_index=*/4));
}

// Verify on the apps grid with a non-zero scroll offset.
TEST_P(ScrollableAppsGridViewTest, VerifyVisibleRangeAfterScrolling) {
  PopulateApps(33);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so choose a number that is not the
  // multiple of 5 as the total item count.
  ASSERT_EQ(5, cols);

  // Scroll the apps grid so that the item views on the first row are hidden.
  // To calculate the scroll offset, the origin of the item view at (row 1,
  // column 0) should be translated into the scroll content's coordinates.
  views::View* item_view = apps_grid_view_->GetItemViewAt(5);
  views::ScrollView* scroll_view = apps_grid_view_->scroll_view_for_test();
  gfx::Point local_origin;
  views::View::ConvertPointToTarget(item_view, scroll_view->contents(),
                                    &local_origin);
  const int offset = local_origin.y() - scroll_view->GetVisibleRect().y();
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(), offset);

  // Verify that in this case the items on row 1 to row 5 are visible.
  EXPECT_TRUE(
      IsIndexRangeExpected(/*first_index=*/cols, /*last_index=*/6 * cols - 1));
}

// Verify visible items' index range by scrolling to the end on a partially
// filled apps grid.
TEST_P(ScrollableAppsGridViewTest,
       VerifyVisibleRangeAfterScrollingToEndPartiallyFilled) {
  constexpr int populated_app_count = 33;
  PopulateApps(populated_app_count);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so choose a number that is not the
  // multiple of 5 as the total item count.
  ASSERT_EQ(5, cols);

  // Scroll to the end.
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
                                 std::numeric_limits<int>::max());

  // Verify that the items on row 3 to row 6 are visible.
  EXPECT_TRUE(IsIndexRangeExpected(/*first_index=*/3 * cols,
                                   /*last_index=*/populated_app_count - 1));
}

// Verify visible items' item index range by scrolling to the end on a full
// apps grid.
TEST_P(ScrollableAppsGridViewTest,
       VerifyVisibleRangeAfterScrollingToEndFilled) {
  constexpr int populated_app_count = 35;
  PopulateApps(populated_app_count);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so choose a column count's multiple as
  // the total item count.
  ASSERT_EQ(5, cols);

  // Scroll to the end.
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
                                 std::numeric_limits<int>::max());

  // Verify that the items on row 3 to row 6 are visible.
  EXPECT_TRUE(IsIndexRangeExpected(/*first_index=*/3 * cols,
                                   /*last_index=*/populated_app_count - 1));
}

// Tests the scrollable apps grid view with app list nudge enabled.
class ScrollableAppsGridViewWithNudgeTest : public ScrollableAppsGridViewTest {
 public:
  // ScrollableAppsGridViewTest:
  void SetUp() override {
    ScrollableAppsGridViewTest::SetUp();
    GetAppListTestHelper()->DisableAppListNudge(false);
  }
};

// Verify on the apps grid with zero scroll offset.
TEST_P(ScrollableAppsGridViewWithNudgeTest, VerifyVisibleRangeByDefault) {
  PopulateApps(33);
  ShowAppList();

  const int cols = apps_grid_view_->cols();

  // Assume that the column count is 5 so choose a number that is not the
  // multiple of 5 as the total item count.
  ASSERT_EQ(5, cols);

  // With the app list reorder nudge is showing, there's enough space to fit
  // only 4 rows of apps in the visible portion of the app list.
  EXPECT_TRUE(
      IsIndexRangeExpected(/*first_index=*/0, /*last_index=*/4 * cols - 1));
}
INSTANTIATE_TEST_SUITE_P(All,
                         ScrollableAppsGridViewWithNudgeTest,
                         testing::Bool());

TEST_P(ScrollableAppsGridViewTest, RecordMetricsForAppLaunchByEntity) {
  base::HistogramTester histograms;
  GetAppListTestHelper()->AddAppListItemsWithCollection(
      AppCollection::kEntertainment, 1);
  AddAppListItem("id1");
  ShowAppList();

  histograms.ExpectUniqueSample(
      "Apps.AppListBubble.AppsPage.AppLaunchesByEntity", AppEntity::kDefaultApp,
      0);
  histograms.ExpectUniqueSample(
      "Apps.AppListBubble.AppsPage.AppLaunchesByEntity",
      AppEntity::kThirdPartyApp, 0);

  LeftClickOn(apps_grid_view_->GetItemViewAt(0));
  histograms.ExpectBucketCount(
      "Apps.AppListBubble.AppsPage.AppLaunchesByEntity", AppEntity::kDefaultApp,
      1);
  histograms.ExpectBucketCount(
      "Apps.AppListBubble.AppsPage.AppLaunchesByEntity",
      AppEntity::kThirdPartyApp, 0);

  LeftClickOn(apps_grid_view_->GetItemViewAt(1));
  histograms.ExpectBucketCount(
      "Apps.AppListBubble.AppsPage.AppLaunchesByEntity", AppEntity::kDefaultApp,
      1);
  histograms.ExpectBucketCount(
      "Apps.AppListBubble.AppsPage.AppLaunchesByEntity",
      AppEntity::kThirdPartyApp, 1);
}

// Verifies that apps visibility is correctly calculated.
TEST_P(ScrollableAppsGridViewTest, AppsVisibility) {
  // Create enough apps so that the launcher can be scrolled.
  PopulateApps(50);
  ShowAppList();

  ASSERT_NE(scroll_view_->GetVisibleBounds(),
            scroll_view_->contents()->GetLocalBounds());

  EXPECT_EQ(0, GetTestAppListClient()->activate_item_above_the_fold());
  EXPECT_EQ(0, GetTestAppListClient()->activate_item_below_the_fold());

  AppListItemView* above_the_fold_item = apps_grid_view_->GetItemViewAt(0);
  GetEventGenerator()->MoveMouseTo(
      above_the_fold_item->GetBoundsInScreen().CenterPoint());
  GetEventGenerator()->ClickLeftButton();

  EXPECT_TRUE(apps_grid_view_->IsAboveTheFold(above_the_fold_item));
  EXPECT_EQ(1, GetTestAppListClient()->activate_item_above_the_fold());
  EXPECT_EQ(0, GetTestAppListClient()->activate_item_below_the_fold());

  AppListItemView* below_the_fold_item = apps_grid_view_->GetItemViewAt(49);

  // Scroll the apps page to the end.
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(), INT_MAX);
  GetEventGenerator()->MoveMouseTo(
      below_the_fold_item->GetBoundsInScreen().CenterPoint());
  GetEventGenerator()->ClickLeftButton();

  EXPECT_FALSE(apps_grid_view_->IsAboveTheFold(below_the_fold_item));
  EXPECT_EQ(1, GetTestAppListClient()->activate_item_above_the_fold());
  EXPECT_EQ(1, GetTestAppListClient()->activate_item_below_the_fold());
}

// Verifies that apps visibility is correctly calculated.
TEST_P(ScrollableAppsGridViewTest, AppsVisibilityOnShow) {
  // Create enough apps so that the launcher can be scrolled.
  PopulateApps(50);
  ShowAppList();

  ASSERT_NE(scroll_view_->GetVisibleBounds(),
            scroll_view_->contents()->GetLocalBounds());

  int apps_above = 0;
  int apps_below = 0;

  for (size_t index = 0; index < 50; index++) {
    if (apps_grid_view_->IsAboveTheFold(
            apps_grid_view_->GetItemViewAt(index))) {
      ++apps_above;
    } else {
      ++apps_below;
    }
  }

  EXPECT_EQ(apps_above, GetTestAppListClient()->items_above_the_fold_count());
  EXPECT_EQ(apps_below, GetTestAppListClient()->items_below_the_fold_count());
}

}  // namespace ash