chromium/ash/public/cpp/pagination/pagination_model_unittest.cc

// Copyright 2012 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/public/cpp/pagination/pagination_model.h"

#include <string>

#include "ash/public/cpp/pagination/pagination_model_observer.h"
#include "base/compiler_specific.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "ui/views/test/widget_test.h"

namespace ash {
namespace test {

class TestPaginationModelObserver : public PaginationModelObserver {
 public:
  TestPaginationModelObserver() = default;

  TestPaginationModelObserver(const TestPaginationModelObserver&) = delete;
  TestPaginationModelObserver& operator=(const TestPaginationModelObserver&) =
      delete;

  ~TestPaginationModelObserver() override = default;

  void Reset() {
    selection_count_ = 0;
    transition_start_count_ = 0;
    transition_end_count_ = 0;
    selected_pages_.clear();
    transition_start_call_count_ = 0;
    transition_ended_call_count_ = 0;
    wait_loop_ = nullptr;
  }

  void set_model(PaginationModel* model) { model_ = model; }
  void set_wait_loop(base::RunLoop* wait_loop) { wait_loop_ = wait_loop; }

  void set_expected_page_selection(int expected_page_selection) {
    expected_page_selection_ = expected_page_selection;
  }
  void set_expected_transition_start(int expected_transition_start) {
    expected_transition_start_ = expected_transition_start;
  }
  void set_expected_transition_end(int expected_transition_end) {
    expected_transition_end_ = expected_transition_end;
  }
  void set_transition_page(int page) { transition_page_ = page; }

  const std::string& selected_pages() const { return selected_pages_; }
  int selection_count() const { return selection_count_; }
  int transition_start_count() const { return transition_start_count_; }
  int transition_end_count() const { return transition_end_count_; }
  int transition_start_call_count() const {
    return transition_start_call_count_;
  }
  int transition_end_call_count() const { return transition_ended_call_count_; }

 private:
  void AppendSelectedPage(int page) {
    if (selected_pages_.length())
      selected_pages_.append(std::string(" "));
    selected_pages_.append(base::NumberToString(page));
  }

  // PaginationModelObserver overrides:
  void TotalPagesChanged(int previous_page_count, int new_page_count) override {
  }
  void SelectedPageChanged(int old_selected, int new_selected) override {
    AppendSelectedPage(new_selected);
    ++selection_count_;
    if (wait_loop_ && expected_page_selection_ &&
        selection_count_ == expected_page_selection_) {
      wait_loop_->Quit();
    }
  }

  void TransitionStarted() override { ++transition_start_call_count_; }

  void TransitionChanged() override {
    if (transition_page_ == -1 ||
        model_->transition().target_page == transition_page_) {
      if (model_->transition().progress == 0)
        ++transition_start_count_;
      if (model_->transition().progress == 1)
        ++transition_end_count_;
    }

    if (!wait_loop_)
      return;

    if ((expected_transition_start_ &&
         transition_start_count_ == expected_transition_start_) ||
        (expected_transition_end_ &&
         transition_end_count_ == expected_transition_end_)) {
      wait_loop_->Quit();
    }
  }

  void TransitionEnded() override { ++transition_ended_call_count_; }

  raw_ptr<PaginationModel, DanglingUntriaged> model_ = nullptr;

  int expected_page_selection_ = 0;
  int expected_transition_start_ = 0;
  int expected_transition_end_ = 0;

  int selection_count_ = 0;
  int transition_start_count_ = 0;
  int transition_end_count_ = 0;

  // Indicate which page index should be counted for |transition_start_count_|
  // and |transition_end_count_|. -1 means all the pages should be counted.
  int transition_page_ = -1;

  std::string selected_pages_;

  int transition_start_call_count_ = 0;
  int transition_ended_call_count_ = 0;
  raw_ptr<base::RunLoop, DanglingUntriaged> wait_loop_ = nullptr;
};

class PaginationModelTest : public views::test::WidgetTest {
 public:
  PaginationModelTest() = default;

  PaginationModelTest(const PaginationModelTest&) = delete;
  PaginationModelTest& operator=(const PaginationModelTest&) = delete;

  ~PaginationModelTest() override = default;

  // testing::Test overrides:
  void SetUp() override {
    views::test::WidgetTest::SetUp();
    widget_.reset(CreateTopLevelPlatformWidget());
    pagination_ = std::make_unique<PaginationModel>(widget_->GetContentsView());
    pagination_->SetTotalPages(5);
    pagination_->SetTransitionDurations(base::Milliseconds(1),
                                        base::Milliseconds(1));
    observer_.set_model(pagination_.get());
    pagination_->AddObserver(&observer_);
  }
  void TearDown() override {
    pagination_->RemoveObserver(&observer_);
    observer_.set_model(NULL);
    widget_.reset();
    views::test::WidgetTest::TearDown();
  }

 protected:
  void SetStartPageAndExpects(int start_page,
                              int expected_selection,
                              int expected_transition_start,
                              int expected_transition_end) {
    observer_.set_wait_loop(nullptr);
    pagination_->SelectPage(start_page, /*animate=*/false);
    observer_.Reset();

    paging_animation_wait_loop_ = std::make_unique<base::RunLoop>();
    observer_.set_wait_loop(paging_animation_wait_loop_.get());

    observer_.set_expected_page_selection(expected_selection);
    observer_.set_expected_transition_start(expected_transition_start);
    observer_.set_expected_transition_end(expected_transition_end);
  }

  void WaitForPagingAnimation() {
    ASSERT_TRUE(paging_animation_wait_loop_);
    paging_animation_wait_loop_->Run();
  }

  void WaitForRevertAnimation() {
    while (pagination()->IsRevertingCurrentTransition()) {
      base::RunLoop run_loop;
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(100));
      run_loop.Run();
    }
  }

  PaginationModel* pagination() { return pagination_.get(); }

  TestPaginationModelObserver observer_;

 private:
  WidgetAutoclosePtr widget_;
  std::unique_ptr<PaginationModel> pagination_;
  std::unique_ptr<base::RunLoop> paging_animation_wait_loop_;
};

TEST_F(PaginationModelTest, SelectPage) {
  pagination()->SelectPage(2, /*animate=*/false);
  pagination()->SelectPage(4, /*animate=*/false);
  pagination()->SelectPage(3, /*animate=*/false);
  pagination()->SelectPage(1, /*animate=*/false);

  EXPECT_EQ(0, observer_.transition_start_count());
  EXPECT_EQ(0, observer_.transition_end_count());
  EXPECT_EQ(4, observer_.selection_count());
  EXPECT_EQ(std::string("2 4 3 1"), observer_.selected_pages());

  // Nothing happens if select the same page.
  pagination()->SelectPage(1, /*animate=*/false);
  EXPECT_EQ(4, observer_.selection_count());
  EXPECT_EQ(std::string("2 4 3 1"), observer_.selected_pages());
}

TEST_F(PaginationModelTest, IsValidPageRelative) {
  pagination()->SelectPage(0, false /*animate*/);
  ASSERT_FALSE(pagination()->IsValidPageRelative(-1));
  ASSERT_FALSE(pagination()->IsValidPageRelative(5));
  ASSERT_TRUE(pagination()->IsValidPageRelative(1));
  ASSERT_TRUE(pagination()->IsValidPageRelative(4));
}

TEST_F(PaginationModelTest, SelectPageAnimated) {
  const int kStartPage = 0;

  // One transition.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SelectPage(1, /*animate=*/true);
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.transition_start_count());
  EXPECT_EQ(1, observer_.transition_end_count());
  EXPECT_EQ(1, observer_.selection_count());
  EXPECT_EQ(std::string("1"), observer_.selected_pages());

  // Two transitions in a row.
  SetStartPageAndExpects(kStartPage, 2, 0, 0);
  pagination()->SelectPage(1, /*animate=*/true);
  pagination()->SelectPage(3, /*animate=*/true);
  WaitForPagingAnimation();
  EXPECT_EQ(2, observer_.transition_start_count());
  EXPECT_EQ(2, observer_.transition_end_count());
  EXPECT_EQ(2, observer_.selection_count());
  EXPECT_EQ(std::string("1 3"), observer_.selected_pages());

  // Transition to same page twice and only one should happen.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SelectPage(1, /*animate=*/true);
  pagination()->SelectPage(1, /*animate=*/true);  // Ignored.
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.transition_start_count());
  EXPECT_EQ(1, observer_.transition_end_count());
  EXPECT_EQ(1, observer_.selection_count());
  EXPECT_EQ(std::string("1"), observer_.selected_pages());

  // More than two transitions and only the first and last would happen.
  SetStartPageAndExpects(kStartPage, 2, 0, 0);
  pagination()->SelectPage(1, /*animate=*/true);
  pagination()->SelectPage(3, /*animate=*/true);  // Ignored
  pagination()->SelectPage(4, /*animate=*/true);  // Ignored
  pagination()->SelectPage(2, /*animate=*/true);
  WaitForPagingAnimation();
  EXPECT_EQ(2, observer_.transition_start_count());
  EXPECT_EQ(2, observer_.transition_end_count());
  EXPECT_EQ(2, observer_.selection_count());
  EXPECT_EQ(std::string("1 2"), observer_.selected_pages());

  // Multiple transitions with one transition that goes back to the original
  // and followed by a new transition. Two transitions would happen. The first
  // one will be reversed by the kStart transition and the second one will be
  // finished.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SelectPage(1, /*animate=*/true);
  pagination()->SelectPage(2, /*animate=*/true);  // Ignored
  pagination()->SelectPage(kStartPage, /*animate=*/true);
  pagination()->SelectPage(3, /*animate=*/true);
  WaitForPagingAnimation();
  EXPECT_EQ(std::string("3"), observer_.selected_pages());
}

TEST_F(PaginationModelTest, SimpleScroll) {
  const int kStartPage = 2;

  // Scroll to the next page (negative delta) and finish it.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  pagination()->EndScroll(false);  // Finish transition
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.selection_count());

  // Scroll to the previous page (positive delta) and finish it.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  pagination()->EndScroll(false);  // Finish transition
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.selection_count());

  // Scroll to the next page (negative delta) and cancel it.
  SetStartPageAndExpects(kStartPage, 0, 1, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  pagination()->EndScroll(true);  // Cancel transition
  WaitForPagingAnimation();
  EXPECT_EQ(0, observer_.selection_count());

  // Scroll to the previous page (position delta) and cancel it.
  SetStartPageAndExpects(kStartPage, 0, 1, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  pagination()->EndScroll(true);  // Cancel transition
  WaitForPagingAnimation();
  EXPECT_EQ(0, observer_.selection_count());
}

TEST_F(PaginationModelTest, ScrollWithTransition) {
  const int kStartPage = 2;

  // Scroll to the next page (negative delta) with a transition in the same
  // direction.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage + 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  EXPECT_EQ(0.6, pagination()->transition().progress);
  pagination()->EndScroll(false);
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.selection_count());

  // Scroll to the next page (negative delta) with a transition in a different
  // direction.
  SetStartPageAndExpects(kStartPage, 0, 1, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage - 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  EXPECT_EQ(0.4, pagination()->transition().progress);
  pagination()->EndScroll(true);

  // Scroll to the previous page (positive delta) with a transition in the same
  // direction.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage - 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  EXPECT_EQ(0.6, pagination()->transition().progress);
  pagination()->EndScroll(false);
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.selection_count());

  // Scroll to the previous page (positive delta) with a transition in a
  // different direction.
  SetStartPageAndExpects(kStartPage, 0, 1, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage + 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  EXPECT_EQ(0.4, pagination()->transition().progress);
  pagination()->EndScroll(true);
}

TEST_F(PaginationModelTest, LongScroll) {
  const int kStartPage = 2;

  // Scroll to the next page (negative delta) with a transition in the same
  // direction. And scroll enough to change page twice.
  SetStartPageAndExpects(kStartPage, 2, 0, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage + 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  EXPECT_EQ(0.6, pagination()->transition().progress);
  pagination()->UpdateScroll(-0.5);
  EXPECT_EQ(1, observer_.selection_count());
  pagination()->UpdateScroll(-0.5);
  EXPECT_EQ(kStartPage + 2, pagination()->transition().target_page);
  pagination()->EndScroll(false);
  WaitForPagingAnimation();
  EXPECT_EQ(2, observer_.selection_count());

  // Scroll to the next page (negative delta) with a transition in a different
  // direction. And scroll enough to revert it and switch page once.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage - 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  EXPECT_EQ(0.4, pagination()->transition().progress);
  pagination()->UpdateScroll(-0.5);  // This clears the transition.
  pagination()->UpdateScroll(-0.5);  // This starts a new transition.
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  pagination()->EndScroll(false);
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.selection_count());

  // Similar cases as above but in the opposite direction.
  // Scroll to the previous page (positive delta) with a transition in the same
  // direction. And scroll enough to change page twice.
  SetStartPageAndExpects(kStartPage, 2, 0, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage - 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  EXPECT_EQ(0.6, pagination()->transition().progress);
  pagination()->UpdateScroll(0.5);
  EXPECT_EQ(1, observer_.selection_count());
  pagination()->UpdateScroll(0.5);
  EXPECT_EQ(kStartPage - 2, pagination()->transition().target_page);
  pagination()->EndScroll(false);
  WaitForPagingAnimation();
  EXPECT_EQ(2, observer_.selection_count());

  // Scroll to the previous page (positive delta) with a transition in a
  // different direction. And scroll enough to revert it and switch page once.
  SetStartPageAndExpects(kStartPage, 1, 0, 0);
  pagination()->SetTransition(PaginationModel::Transition(kStartPage + 1, 0.5));
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  EXPECT_EQ(0.4, pagination()->transition().progress);
  pagination()->UpdateScroll(0.5);  // This clears the transition.
  pagination()->UpdateScroll(0.5);  // This starts a new transition.
  EXPECT_EQ(kStartPage - 1, pagination()->transition().target_page);
  pagination()->EndScroll(false);
  WaitForPagingAnimation();
  EXPECT_EQ(1, observer_.selection_count());
}

TEST_F(PaginationModelTest, FireTransitionZero) {
  const int kStartPage = 2;

  // Scroll to next page then revert the scroll and make sure transition
  // progress 0 is fired when previous scroll is cleared.
  SetStartPageAndExpects(kStartPage, 0, 0, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);

  int target_page = kStartPage + 1;
  EXPECT_EQ(target_page, pagination()->transition().target_page);
  observer_.set_transition_page(target_page);

  pagination()->UpdateScroll(0.2);  // This clears the transition.
  EXPECT_EQ(1, observer_.transition_start_count());
  pagination()->EndScroll(true);  // Cancel transition.

  // Similar to above but in the other direction.
  SetStartPageAndExpects(kStartPage, 0, 0, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(0.1);

  target_page = kStartPage - 1;
  EXPECT_EQ(target_page, pagination()->transition().target_page);
  observer_.set_transition_page(target_page);

  pagination()->UpdateScroll(-0.2);  // This clears the transition.
  EXPECT_EQ(1, observer_.transition_start_count());
  pagination()->EndScroll(true);  // Cancel transition.
}

TEST_F(PaginationModelTest, SelectedPageIsLost) {
  pagination()->SetTotalPages(2);
  // The selected page is set to 0 once the total number of pages are set.
  EXPECT_EQ(0, pagination()->selected_page());

  pagination()->SelectPage(1, /*animate=*/false);
  EXPECT_EQ(1, pagination()->selected_page());

  // The selected page is unchanged if it's still valid.
  pagination()->SetTotalPages(3);
  EXPECT_EQ(1, pagination()->selected_page());
  pagination()->SetTotalPages(2);
  EXPECT_EQ(1, pagination()->selected_page());

  // But if the currently selected_page exceeds the total number of pages,
  // it automatically switches to the last page.
  pagination()->SetTotalPages(1);
  EXPECT_EQ(0, pagination()->selected_page());
}

TEST_F(PaginationModelTest, SelectPageRelativeBeginning) {
  // Test starts with 5 pages. Select Page 1.
  pagination()->SelectPage(1, false);

  pagination()->SelectPageRelative(-1, false);
  EXPECT_EQ(0, pagination()->selected_page());
}

TEST_F(PaginationModelTest, SelectPageRelativeMiddle) {
  // Test starts with 5 pages. Select page 2.
  pagination()->SelectPage(2, false);

  pagination()->SelectPageRelative(-1, false);
  EXPECT_EQ(1, pagination()->selected_page());

  pagination()->SelectPageRelative(1, false);
  EXPECT_EQ(2, pagination()->selected_page());
}

// Tests that only one TransitionEnd is called for the invalid page selection
// and no TransitionEnd happens for the reverse animation of the invalid page
// selection..
TEST_F(PaginationModelTest, NoTransitionEndForRevertingAnimation) {
  // Attempts to go beyond the first page.
  SetStartPageAndExpects(0, 0, 0, 1);
  pagination()->SelectPageRelative(-1, /*animate=*/true);
  WaitForPagingAnimation();
  WaitForRevertAnimation();
  EXPECT_EQ(1, observer_.transition_start_call_count());
  EXPECT_EQ(1, observer_.transition_end_call_count());

  // Attempts to go beyond the last page.
  SetStartPageAndExpects(pagination()->total_pages() - 1, 0, 0, 1);
  pagination()->SelectPageRelative(1, /*animate=*/true);
  WaitForPagingAnimation();
  WaitForRevertAnimation();
  EXPECT_EQ(1, observer_.transition_start_call_count());
  EXPECT_EQ(1, observer_.transition_end_call_count());
}

// Tests that a canceled scroll will call both TransitionStart and
// TransitionEnd.
TEST_F(PaginationModelTest, CancelAnimationHasOneTransitionEnd) {
  const int kStartPage = 2;

  // Scroll to the next page (negative delta) and cancel it.
  SetStartPageAndExpects(kStartPage, 0, 1, 0);
  pagination()->StartScroll();
  pagination()->UpdateScroll(-0.1);
  EXPECT_EQ(kStartPage + 1, pagination()->transition().target_page);
  pagination()->EndScroll(true);  // Cancel transition
  WaitForPagingAnimation();
  EXPECT_EQ(0, observer_.selection_count());

  EXPECT_EQ(1, observer_.transition_start_call_count());
  EXPECT_EQ(1, observer_.transition_end_call_count());
}

}  // namespace test
}  // namespace ash