chromium/ash/system/unified/feature_tiles_container_view_unittest.cc

// Copyright 2022 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/system/unified/feature_tiles_container_view.h"

#include "ash/constants/quick_settings_catalogs.h"
#include "ash/public/cpp/pagination/pagination_model.h"
#include "ash/shell.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/unified/feature_pod_button.h"
#include "ash/system/unified/feature_pod_controller_base.h"
#include "ash/system/unified/feature_tile.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/test/views_test_utils.h"

namespace ash {

namespace {

class MockFeaturePodController : public FeaturePodControllerBase {
 public:
  MockFeaturePodController() = default;
  MockFeaturePodController(const MockFeaturePodController&) = delete;
  MockFeaturePodController& operator=(const MockFeaturePodController&) = delete;
  ~MockFeaturePodController() override = default;

  std::unique_ptr<FeatureTile> CreateTile(bool compact = false) override {
    auto tile = std::make_unique<FeatureTile>(
        base::BindRepeating(&FeaturePodControllerBase::OnIconPressed,
                            weak_ptr_factory_.GetWeakPtr()),
        /*togglable=*/true,
        compact ? FeatureTile::TileType::kCompact
                : FeatureTile::TileType::kPrimary);
    tile->SetVectorIcon(vector_icons::kDogfoodIcon);
    return tile;
  }

  QsFeatureCatalogName GetCatalogName() override {
    return QsFeatureCatalogName::kUnknown;
  }

  void OnIconPressed() override {}
  void OnLabelPressed() override {}

 private:
  base::WeakPtrFactory<MockFeaturePodController> weak_ptr_factory_{this};
};

constexpr int kMaxPrimaryTilesPerRow = 2;

}  // namespace

class FeatureTilesContainerViewTest : public AshTestBase,
                                      public views::ViewObserver {
 public:
  FeatureTilesContainerViewTest() = default;
  FeatureTilesContainerViewTest(const FeatureTilesContainerViewTest&) = delete;
  FeatureTilesContainerViewTest& operator=(
      const FeatureTilesContainerViewTest&) = delete;
  ~FeatureTilesContainerViewTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    tray_model_ =
        base::MakeRefCounted<UnifiedSystemTrayModel>(/*shelf=*/nullptr);
    tray_controller_ =
        std::make_unique<UnifiedSystemTrayController>(tray_model_.get());
    widget_ = CreateFramelessTestWidget();
    widget_->SetFullscreen(true);

    container_ = widget_->SetContentsView(
        std::make_unique<FeatureTilesContainerView>(tray_controller_.get()));
    container_->AddObserver(this);
  }

  void TearDown() override {
    container_->RemoveObserver(this);
    widget_.reset();
    tray_controller_.reset();
    tray_model_.reset();

    AshTestBase::TearDown();
  }

  void PressTab() {
    ui::test::EventGenerator generator(Shell::GetPrimaryRootWindow());
    generator.PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_NONE);
  }

  void PressShiftTab() {
    ui::test::EventGenerator generator(Shell::GetPrimaryRootWindow());
    generator.PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
  }

  FeatureTilesContainerView* container() { return container_; }

  PaginationModel* pagination_model() { return container()->pagination_model_; }

  void AddTiles(std::vector<std::unique_ptr<FeatureTile>> tiles) {
    container()->AddTiles(std::move(tiles));
  }

  void SetRowsFromHeight(int max_height) {
    return container()->SetRowsFromHeight(max_height);
  }

  int CalculateRowsFromHeight(int height) {
    return container()->CalculateRowsFromHeight(height);
  }

  void AdjustRowsForMediaViewVisibility(int height) {
    container()->AdjustRowsForMediaViewVisibility(true, height);
  }

  int GetRowCount() { return container()->row_count(); }

  int GetPageCount() { return container()->page_count(); }

  int GetVisibleCount() { return container()->GetVisibleFeatureTileCount(); }

  std::vector<raw_ptr<views::View, VectorExperimental>> pages() {
    return container()->children();
  }

  // Fills the container with a number of `pages` given the max amount of
  // displayable primary tiles per page.
  void FillContainerWithPrimaryTiles(int pages) {
    auto mock_controller = std::make_unique<MockFeaturePodController>();
    std::vector<std::unique_ptr<FeatureTile>> tiles;

    size_t number_of_tiles =
        pages * container()->displayable_rows() * kMaxPrimaryTilesPerRow;

    while (tiles.size() < number_of_tiles) {
      tiles.push_back(mock_controller->CreateTile());
    }
    AddTiles(std::move(tiles));

    EXPECT_EQ(pages, GetPageCount());
    EXPECT_EQ(pages, pagination_model()->total_pages());
  }

 private:
  std::unique_ptr<views::Widget> widget_;
  std::unique_ptr<UnifiedSystemTrayController> tray_controller_;
  scoped_refptr<UnifiedSystemTrayModel> tray_model_;
  raw_ptr<FeatureTilesContainerView, DanglingUntriaged> container_;
};

// Tests `CalculateRowsFromHeight()` which returns the number of max displayable
// feature tile rows given the available height.
TEST_F(FeatureTilesContainerViewTest, DisplayableRows) {
  int row_height = kFeatureTileHeight;

  // Expect max number of rows even if available height could fit another row.
  EXPECT_EQ(kFeatureTileMaxRows,
            CalculateRowsFromHeight((kFeatureTileMaxRows + 1) * row_height));

  // Expect appropriate number of rows with available height.
  EXPECT_EQ(3, CalculateRowsFromHeight(3 * row_height));

  // Expect min number of rows even with zero height.
  EXPECT_EQ(kFeatureTileMinRows, CalculateRowsFromHeight(0));
}

TEST_F(FeatureTilesContainerViewTest,
       DisplayableRowsIsLessWhenMediaViewIsShowing) {
  int row_height = kFeatureTileHeight;
  // Set height to equivalent of max+1 rows.
  const int max_height = (kFeatureTileMaxRows + 1) * row_height;

  // Expect default to cap at `kFeatureTileMaxRows`.
  EXPECT_EQ(kFeatureTileMaxRows, CalculateRowsFromHeight(max_height));

  AdjustRowsForMediaViewVisibility(max_height);

  // Expect height to be capped at `kFeatureTileMaxRowsWhenMediaViewIsShowing`
  // when media view is showing.
  EXPECT_EQ(kFeatureTileMaxRowsWhenMediaViewIsShowing,
            CalculateRowsFromHeight(max_height));
}

// Tests that rows are dynamically added by adding `FeatureTile` elements to the
// container.
TEST_F(FeatureTilesContainerViewTest, FeatureTileRows) {
  auto mock_controller = std::make_unique<MockFeaturePodController>();

  // Expect one row by adding two primary tiles.
  std::vector<std::unique_ptr<FeatureTile>> two_primary_tiles;
  two_primary_tiles.push_back(mock_controller->CreateTile());
  two_primary_tiles.push_back(mock_controller->CreateTile());
  container()->AddTiles(std::move(two_primary_tiles));
  EXPECT_EQ(1, GetRowCount());
  EXPECT_EQ(2, GetVisibleCount());

  // Add one primary, and two compact tiles. This should create a second row.
  std::vector<std::unique_ptr<FeatureTile>> one_primary_two_compact_tiles;
  one_primary_two_compact_tiles.push_back(mock_controller->CreateTile());
  one_primary_two_compact_tiles.push_back(
      mock_controller->CreateTile(/*compact=*/true));
  one_primary_two_compact_tiles.push_back(
      mock_controller->CreateTile(/*compact=*/true));
  container()->AddTiles(std::move(one_primary_two_compact_tiles));
  EXPECT_EQ(2, GetRowCount());
  EXPECT_EQ(5, GetVisibleCount());

  // Add one primary tile, this should result in a third row.
  std::vector<std::unique_ptr<FeatureTile>> one_primary_tile;
  one_primary_tile.push_back(mock_controller->CreateTile());
  container()->AddTiles(std::move(one_primary_tile));
  EXPECT_EQ(3, GetRowCount());
  EXPECT_EQ(6, GetVisibleCount());
}

TEST_F(FeatureTilesContainerViewTest, ChangeTileVisibility) {
  // Create 3 full-size tiles. Normally they would require 2 rows.
  auto mock_controller = std::make_unique<MockFeaturePodController>();
  std::unique_ptr<FeatureTile> tile1 = mock_controller->CreateTile();
  std::unique_ptr<FeatureTile> tile2 = mock_controller->CreateTile();
  std::unique_ptr<FeatureTile> tile3 = mock_controller->CreateTile();

  // Make the first tile invisible.
  FeatureTile* tile1_ptr = tile1.get();
  tile1_ptr->SetVisible(false);

  // Add the tiles to the container.
  std::vector<std::unique_ptr<FeatureTile>> tiles;
  tiles.push_back(std::move(tile1));
  tiles.push_back(std::move(tile2));
  tiles.push_back(std::move(tile3));
  AddTiles(std::move(tiles));

  // Only one row is created because the first tile is not visible.
  EXPECT_EQ(1, GetRowCount());
  EXPECT_EQ(2, GetVisibleCount());

  // Making the tile visible causes a second row to be created.
  tile1_ptr->SetVisible(true);
  EXPECT_EQ(2, GetRowCount());
  EXPECT_EQ(3, GetVisibleCount());

  // Making the tile invisible causes the second row to be removed.
  tile1_ptr->SetVisible(false);
  EXPECT_EQ(1, GetRowCount());
  EXPECT_EQ(2, GetVisibleCount());
}

TEST_F(FeatureTilesContainerViewTest, PageCountUpdated) {
  auto mock_controller = std::make_unique<MockFeaturePodController>();

  // Set the container height to have two displayable rows per page.
  SetRowsFromHeight(kFeatureTileHeight * 2);

  std::vector<std::unique_ptr<FeatureTile>> tiles;

  // Get pointer of one tile so we can make invisible later.
  std::unique_ptr<FeatureTile> tile1 = mock_controller->CreateTile();
  FeatureTile* tile1_ptr = tile1.get();
  tiles.push_back(std::move(tile1));

  // Add a total of five tiles to the container.
  while (tiles.size() < 5) {
    tiles.push_back(mock_controller->CreateTile());
  }

  // Since a row fits two primary tiles, expect two pages for five primary
  // tiles.
  AddTiles(std::move(tiles));
  EXPECT_EQ(2, GetPageCount());
  EXPECT_EQ(5, GetVisibleCount());

  // Expect change in page count after updating visibility of a tile.
  tile1_ptr->SetVisible(false);
  EXPECT_EQ(1, GetPageCount());
  EXPECT_EQ(4, GetVisibleCount());

  // Expect change in page count after updating max displayable rows by updating
  // the available height.
  SetRowsFromHeight(kFeatureTileHeight);
  EXPECT_EQ(2, GetPageCount());
  EXPECT_EQ(4, GetVisibleCount());
}

// TODO(b/263185068): Use EventGenerator.
TEST_F(FeatureTilesContainerViewTest, PaginationGesture) {
  constexpr int kNumberOfPages = 4;
  FillContainerWithPrimaryTiles(kNumberOfPages);

  gfx::Point container_origin = container()->GetBoundsInScreen().origin();
  ui::GestureEvent swipe_left_begin(
      container_origin.x(), container_origin.y(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureScrollBegin, -1, 0));
  ui::GestureEvent swipe_left_update(
      container_origin.x(), container_origin.y(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, -1000, 0));
  ui::GestureEvent swipe_right_begin(
      container_origin.x(), container_origin.y(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureScrollBegin, 1, 0));
  ui::GestureEvent swipe_right_update(
      container_origin.x(), container_origin.y(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, 1000, 0));
  ui::GestureEvent swipe_end(
      container_origin.x(), container_origin.y(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureEnd));

  int previous_page = pagination_model()->selected_page();

  // Swipe left takes to next page
  for (int i = 0; i < kNumberOfPages - 1; i++) {
    // Simulate swipe left
    container()->OnGestureEvent(&swipe_left_begin);
    container()->OnGestureEvent(&swipe_left_update);
    container()->OnGestureEvent(&swipe_end);

    int current_page = pagination_model()->selected_page();
    // Expect next page
    EXPECT_EQ(previous_page + 1, current_page);
    previous_page = current_page;
  }

  // Swipe left on last page does nothing
  container()->OnGestureEvent(&swipe_left_begin);
  container()->OnGestureEvent(&swipe_left_update);
  container()->OnGestureEvent(&swipe_end);

  EXPECT_EQ(previous_page, pagination_model()->selected_page());

  // Swipe right takes to previous page
  for (int i = 0; i < kNumberOfPages - 1; i++) {
    // Simulate swipe right
    container()->OnGestureEvent(&swipe_right_begin);
    container()->OnGestureEvent(&swipe_right_update);
    container()->OnGestureEvent(&swipe_end);

    int current_page = pagination_model()->selected_page();
    // Expect previous page
    EXPECT_EQ(previous_page - 1, current_page);
    previous_page = current_page;
  }

  // Swipe right on first page does nothing
  container()->OnGestureEvent(&swipe_right_begin);
  container()->OnGestureEvent(&swipe_right_update);
  container()->OnGestureEvent(&swipe_end);

  EXPECT_EQ(previous_page, pagination_model()->selected_page());
}

// TODO(b/263185068): Use EventGenerator.
TEST_F(FeatureTilesContainerViewTest, PaginationScroll) {
  constexpr int kNumberOfFingers = 2;
  constexpr int kNumberOfPages = 4;
  FillContainerWithPrimaryTiles(kNumberOfPages);

  gfx::Point container_origin = container()->GetBoundsInScreen().origin();

  ui::ScrollEvent fling_up_start(ui::EventType::kScrollFlingStart,
                                 container_origin, base::TimeTicks(), 0, 0, 100,
                                 0, 10, kNumberOfFingers);

  ui::ScrollEvent fling_down_start(ui::EventType::kScrollFlingStart,
                                   container_origin, base::TimeTicks(), 0, 0,
                                   -100, 0, 10, kNumberOfFingers);

  ui::ScrollEvent fling_cancel(ui::EventType::kScrollFlingCancel,
                               container_origin, base::TimeTicks(), 0, 0, 0, 0,
                               0, kNumberOfFingers);

  int previous_page = pagination_model()->selected_page();

  // Scroll down takes to next page
  for (int i = 0; i < kNumberOfPages - 1; i++) {
    // Simulate Scroll left
    container()->OnScrollEvent(&fling_down_start);
    container()->OnScrollEvent(&fling_cancel);
    pagination_model()->FinishAnimation();

    int current_page = pagination_model()->selected_page();
    // Expect next page
    EXPECT_EQ(previous_page + 1, current_page);
    previous_page = current_page;
  }

  // Scroll up takes to previous page
  for (int i = 0; i < kNumberOfPages - 1; i++) {
    // Simulate Scroll up
    container()->OnScrollEvent(&fling_up_start);
    container()->OnScrollEvent(&fling_cancel);
    pagination_model()->FinishAnimation();

    int current_page = pagination_model()->selected_page();
    // Expect previous page
    EXPECT_EQ(previous_page - 1, current_page);
    previous_page = current_page;
  }
}

// TODO(b/263185068): Use EventGenerator.
TEST_F(FeatureTilesContainerViewTest, PaginationMouseWheel) {
  constexpr int kNumberOfPages = 4;
  FillContainerWithPrimaryTiles(kNumberOfPages);

  gfx::Point container_origin = container()->GetBoundsInScreen().origin();
  ui::MouseWheelEvent wheel_up(gfx::Vector2d(0, 1000), container_origin,
                               container_origin, base::TimeTicks(), 0, 0);

  ui::MouseWheelEvent wheel_down(gfx::Vector2d(0, -1000), container_origin,
                                 container_origin, base::TimeTicks(), 0, 0);

  int previous_page = pagination_model()->selected_page();

  // Mouse wheel down takes to next page
  for (int i = 0; i < kNumberOfPages - 1; i++) {
    // Simulate mouse wheel down
    container()->OnMouseWheel(wheel_down);
    pagination_model()->FinishAnimation();

    int current_page = pagination_model()->selected_page();
    // Expect next page
    EXPECT_EQ(previous_page + 1, current_page);
    previous_page = current_page;
  }

  // Mouse wheel up takes to previous page
  for (int i = 0; i < kNumberOfPages - 1; i++) {
    // Simulate mouse wheel up
    container()->OnMouseWheel(wheel_up);
    pagination_model()->FinishAnimation();

    int current_page = pagination_model()->selected_page();
    // Expect previous page
    EXPECT_EQ(previous_page - 1, current_page);
    previous_page = current_page;
  }
}

TEST_F(FeatureTilesContainerViewTest, SwitchPageWithFocus) {
  FillContainerWithPrimaryTiles(/*pages=*/2);

  // View starts at page with index zero.
  EXPECT_EQ(0, pagination_model()->selected_page());

  // Tab until the `selected_page` index changes to 1.
  while (pagination_model()->selected_page() == 0) {
    PressTab();
  }
  EXPECT_EQ(1, pagination_model()->selected_page());

  // Pressing shift tab returns to the previous page.
  PressShiftTab();
  EXPECT_EQ(0, pagination_model()->selected_page());
}

TEST_F(FeatureTilesContainerViewTest, PaginationTransition) {
  FillContainerWithPrimaryTiles(/*pages=*/3);
  views::test::RunScheduledLayout(container());

  gfx::Rect initial_bounds = pages()[0]->bounds();
  gfx::Rect current_bounds;
  gfx::Rect previous_bounds = initial_bounds;

  // Page bounds should slide to the left during a transition to the next page.
  PaginationModel::Transition transition(
      pagination_model()->selected_page() + 1, 0);

  for (double i = 0.1; i <= 1.0; i += 0.1) {
    transition.progress = i;
    pagination_model()->SetTransition(transition);

    current_bounds = pages()[0]->bounds();

    EXPECT_LT(current_bounds.x(), previous_bounds.x());
    EXPECT_EQ(current_bounds.y(), previous_bounds.y());

    previous_bounds = current_bounds;
  }

  // Page position after the transition ends should be a page offset to the
  // left.
  int page_offset = kWideTrayMenuWidth;
  gfx::Rect final_bounds =
      gfx::Rect(initial_bounds.x() - page_offset, initial_bounds.y(),
                initial_bounds.width(), initial_bounds.height());
  pagination_model()->SelectPage(1, false);
  views::test::RunScheduledLayout(container());
  EXPECT_EQ(final_bounds, pages()[0]->bounds());
}

}  // namespace ash