// 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