blob: fe56a7159fbba3fcac2ac455ae56f102bb9f16e6 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.car.widget;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeDown;
import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.matcher.ViewMatchers;
import android.support.test.filters.MediumTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import androidx.car.test.R;
/** Unit tests for {@link PagedListView}. */
@RunWith(AndroidJUnit4.class)
@MediumTest
public final class PagedListViewTest {
/**
* Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
* {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
* to ITEMS_PER_PAGE * desired_pages.
* Actual value does not matter.
*/
private static final int ITEMS_PER_PAGE = 5;
@Rule
public ActivityTestRule<PagedListViewTestActivity> mActivityRule =
new ActivityTestRule<>(PagedListViewTestActivity.class);
private PagedListViewTestActivity mActivity;
private PagedListView mPagedListView;
@Before
public void setUp() {
mActivity = mActivityRule.getActivity();
mPagedListView = mActivity.findViewById(R.id.paged_list_view);
// Using deprecated Espresso methods instead of calling it on the IdlingRegistry because
// the latter does not seem to work as reliably. Specifically, on the latter, it does
// not always register and unregister.
Espresso.registerIdlingResources(new PagedListViewScrollingIdlingResource(mPagedListView));
}
@After
public void tearDown() {
for (IdlingResource idlingResource : Espresso.getIdlingResources()) {
Espresso.unregisterIdlingResources(idlingResource);
}
}
/** Returns {@code true} if the testing device has the automotive feature flag. */
private boolean isAutoDevice() {
PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
/** Sets up {@link #mPagedListView} with the given number of items. */
private void setUpPagedListView(int itemCount) {
try {
mActivityRule.runOnUiThread(() -> {
mPagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
mPagedListView.setAdapter(
new TestAdapter(itemCount, mPagedListView.getMeasuredHeight()));
});
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException(throwable);
}
}
@Test
public void testScrollBarIsInvisibleIfItemsDoNotFillOnePage() {
if (!isAutoDevice()) {
return;
}
setUpPagedListView(1 /* itemCount */);
onView(withId(R.id.paged_scroll_view)).check(matches(not(isDisplayed())));
}
@Test
public void testPageUpButtonDisabledAtTop() {
if (!isAutoDevice()) {
return;
}
int itemCount = ITEMS_PER_PAGE * 3;
setUpPagedListView(itemCount);
// Initially page_up button is disabled.
onView(withId(R.id.page_up)).check(matches(not(isEnabled())));
// Moving down, should enable the up bottom.
onView(withId(R.id.page_down)).perform(click());
onView(withId(R.id.page_up)).check(matches(isEnabled()));
// Move back up; this should disable the up bottom again.
onView(withId(R.id.page_up)).perform(click())
.check(matches(not(isEnabled())));
}
@Test
public void testItemSnappedToTopOfListOnScroll() throws InterruptedException {
if (!isAutoDevice()) {
return;
}
// 2.5 so last page is not full
setUpPagedListView((int) (ITEMS_PER_PAGE * 2.5 /* itemCount */));
// Going down one page and first item is snapped to top
onView(withId(R.id.page_down)).perform(click());
verifyItemSnappedToListTop();
}
@Test
public void testLastItemSnappedWhenBottomReached() {
if (!isAutoDevice()) {
return;
}
// 2.5 so last page is not full
setUpPagedListView((int) (ITEMS_PER_PAGE * 2.5 /* itemCount */));
// Go down 2 pages so the bottom is reached.
onView(withId(R.id.page_down)).perform(click());
onView(withId(R.id.page_down)).perform(click()).check(matches(not(isEnabled())));
LinearLayoutManager layoutManager =
(LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager();
// Check that the last item is completely visible.
assertEquals(layoutManager.findLastCompletelyVisibleItemPosition(),
layoutManager.getItemCount() - 1);
}
@Test
public void testSwipeDownKeepsItemSnappedToTopOfList() {
setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
// Go down one page, then swipe down (going up).
onView(withId(R.id.recycler_view)).perform(scrollToPosition(ITEMS_PER_PAGE));
onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition(ITEMS_PER_PAGE, swipeDown()));
verifyItemSnappedToListTop();
}
@Test
public void testSwipeUpKeepsItemSnappedToTopOfList() {
setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
// Swipe up (going down).
onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition(ITEMS_PER_PAGE, swipeUp()));
verifyItemSnappedToListTop();
}
@Test
public void testPageUpAndDownMoveSameDistance() {
if (!isAutoDevice()) {
return;
}
setUpPagedListView(ITEMS_PER_PAGE * 10);
// Move down one page so there will be sufficient pages for up and downs.
onView(withId(R.id.page_down)).perform(click());
LinearLayoutManager layoutManager =
(LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager();
int topPosition = layoutManager.findFirstVisibleItemPosition();
for (int i = 0; i < 3; i++) {
onView(withId(R.id.page_down)).perform(click());
onView(withId(R.id.page_up)).perform(click());
}
assertThat(layoutManager.findFirstVisibleItemPosition(), is(equalTo(topPosition)));
}
@Test
public void setItemSpacing() throws Throwable {
if (!isAutoDevice()) {
return;
}
final int itemCount = 3;
setUpPagedListView(itemCount /* itemCount */);
RecyclerView.LayoutManager layoutManager =
mPagedListView.getRecyclerView().getLayoutManager();
// Initial spacing is 0.
final View[] views = new View[itemCount];
mActivityRule.runOnUiThread(() -> {
for (int i = 0; i < layoutManager.getChildCount(); i++) {
views[i] = layoutManager.getChildAt(i);
}
});
for (int i = 0; i < itemCount - 1; i++) {
assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
}
// Setting item spacing causes layout change.
// Implicitly wait for layout by making two calls in UI thread.
final int itemSpacing = 10;
mActivityRule.runOnUiThread(() -> {
mPagedListView.setItemSpacing(itemSpacing);
});
mActivityRule.runOnUiThread(() -> {
for (int i = 0; i < layoutManager.getChildCount(); i++) {
views[i] = layoutManager.getChildAt(i);
}
});
for (int i = 0; i < itemCount - 1; i++) {
assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(itemSpacing)));
}
// Re-setting spacing back to 0 also works.
mActivityRule.runOnUiThread(() -> {
mPagedListView.setItemSpacing(0);
});
mActivityRule.runOnUiThread(() -> {
for (int i = 0; i < layoutManager.getChildCount(); i++) {
views[i] = layoutManager.getChildAt(i);
}
});
for (int i = 0; i < itemCount - 1; i++) {
assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
}
}
@Test
@UiThreadTest
public void testSetScrollBarButtonIcons() throws Throwable {
if (!isAutoDevice()) {
return;
}
// Set up a pagedListView with a large item count to ensure the scroll bar buttons are
// always showing.
setUpPagedListView(100 /* itemCount */);
Drawable upDrawable = mActivity.getDrawable(R.drawable.ic_thumb_up);
mPagedListView.setUpButtonIcon(upDrawable);
ImageView upButton = mPagedListView.findViewById(R.id.page_up);
ViewMatchers.assertThat(upButton.getDrawable().getConstantState(),
is(equalTo(upDrawable.getConstantState())));
Drawable downDrawable = mActivity.getDrawable(R.drawable.ic_thumb_down);
mPagedListView.setDownButtonIcon(downDrawable);
ImageView downButton = mPagedListView.findViewById(R.id.page_down);
ViewMatchers.assertThat(downButton.getDrawable().getConstantState(),
is(equalTo(downDrawable.getConstantState())));
}
@Test
public void testSettingAndResettingScrollbarColor() {
setUpPagedListView(0);
final int color = R.color.car_teal_700;
// Setting non-zero res ID changes color.
mPagedListView.setScrollbarColor(color);
assertThat(((ColorDrawable)
mPagedListView.mScrollBarView.mScrollThumb.getBackground()).getColor(),
is(equalTo(InstrumentationRegistry.getContext().getColor(color))));
// Resets to default color.
mPagedListView.resetScrollbarColor();
assertThat(((ColorDrawable)
mPagedListView.mScrollBarView.mScrollThumb.getBackground()).getColor(),
is(equalTo(InstrumentationRegistry.getContext().getColor(
R.color.car_scrollbar_thumb))));
}
@Test
public void testSettingScrollbarColorIgnoresDayNightStyle() {
setUpPagedListView(0);
final int color = R.color.car_teal_700;
mPagedListView.setScrollbarColor(color);
for (int style : new int[] {DayNightStyle.AUTO, DayNightStyle.AUTO_INVERSE,
DayNightStyle.FORCE_NIGHT, DayNightStyle.FORCE_DAY}) {
mPagedListView.setDayNightStyle(style);
assertThat(((ColorDrawable)
mPagedListView.mScrollBarView.mScrollThumb.getBackground()).getColor(),
is(equalTo(InstrumentationRegistry.getContext().getColor(color))));
}
}
@Test
public void testDefaultScrollBarTopMargin() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
onView(withId(R.id.paged_scroll_view)).check(matches(withTopMargin(0)));
}
@Test
public void testSetScrollbarTopMargin() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
int topMargin = 100;
mPagedListView.setScrollBarTopMargin(topMargin);
onView(withId(R.id.paged_scroll_view)).check(matches(withTopMargin(topMargin)));
}
@Test
public void testSetGutterNone() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.NONE);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(0)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(0)));
}
@Test
public void testSetGutterStart() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.START);
Resources res = InstrumentationRegistry.getContext().getResources();
int gutterSize = res.getDimensionPixelSize(R.dimen.car_margin);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(gutterSize)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(0)));
}
@Test
public void testSetGutterEnd() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.END);
Resources res = InstrumentationRegistry.getContext().getResources();
int gutterSize = res.getDimensionPixelSize(R.dimen.car_margin);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(0)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(gutterSize)));
}
@Test
public void testSetGutterBoth() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.BOTH);
Resources res = InstrumentationRegistry.getContext().getResources();
int gutterSize = res.getDimensionPixelSize(R.dimen.car_margin);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(gutterSize)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(gutterSize)));
}
@Test
public void testSetGutterSizeNone() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.NONE);
mPagedListView.setGutterSize(120);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(0)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(0)));
}
@Test
public void testSetGutterSizeStart() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.START);
int gutterSize = 120;
mPagedListView.setGutterSize(gutterSize);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(gutterSize)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(0)));
}
@Test
public void testSetGutterSizeEnd() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.END);
int gutterSize = 120;
mPagedListView.setGutterSize(gutterSize);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(0)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(gutterSize)));
}
@Test
public void testSetGutterSizeBoth() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
mPagedListView.setGutter(PagedListView.Gutter.BOTH);
int gutterSize = 120;
mPagedListView.setGutterSize(gutterSize);
assertThat(mPagedListView.getRecyclerView().getPaddingStart(), is(equalTo(gutterSize)));
assertThat(mPagedListView.getRecyclerView().getPaddingEnd(), is(equalTo(gutterSize)));
}
@Test
public void setDefaultScrollBarContainerWidth() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
Resources res = InstrumentationRegistry.getContext().getResources();
int defaultWidth = res.getDimensionPixelSize(R.dimen.car_margin);
onView(withId(R.id.paged_scroll_view)).check(matches(withWidth(defaultWidth)));
}
@Test
public void testSetScrollBarContainerWidth() {
if (!isAutoDevice()) {
return;
}
// Just need enough items to ensure the scroll bar is showing.
setUpPagedListView(ITEMS_PER_PAGE * 10);
int scrollBarContainerWidth = 120;
mPagedListView.setScrollBarContainerWidth(scrollBarContainerWidth);
onView(withId(R.id.paged_scroll_view)).check(matches(withWidth(scrollBarContainerWidth)));
}
private static String itemText(int index) {
return "Data " + index;
}
/**
* Checks that the first item in the list is completely shown and no part of a previous item
* is shown.
*/
private void verifyItemSnappedToListTop() {
LinearLayoutManager layoutManager =
(LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager();
int firstVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
if (firstVisiblePosition > 1) {
int lastInPreviousPagePosition = firstVisiblePosition - 1;
onView(withText(itemText(lastInPreviousPagePosition)))
.check(doesNotExist());
}
}
/** A base adapter that will handle inflating the test view and binding data to it. */
private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
private List<String> mData;
private int mParentHeight;
TestAdapter(int itemCount, int parentHeight) {
mData = new ArrayList<>();
for (int i = 0; i < itemCount; i++) {
mData.add(itemText(i));
}
mParentHeight = parentHeight;
}
@Override
public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
return new TestViewHolder(inflater, parent);
}
@Override
public void onBindViewHolder(TestViewHolder holder, int position) {
// Calculate height for an item so one page fits ITEMS_PER_PAGE items.
int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
holder.itemView.setMinimumHeight(height);
holder.bind(mData.get(position));
}
@Override
public int getItemCount() {
return mData.size();
}
}
private class TestViewHolder extends RecyclerView.ViewHolder {
private TextView mTextView;
TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
mTextView = itemView.findViewById(R.id.text_view);
}
public void bind(String text) {
mTextView.setText(text);
}
}
/**
* An {@link IdlingResource} that will prevent assertions from running while the
* {@link #mPagedListView} is scrolling.
*/
private class PagedListViewScrollingIdlingResource implements IdlingResource {
private boolean mIdle = true;
private ResourceCallback mResourceCallback;
PagedListViewScrollingIdlingResource(PagedListView pagedListView) {
pagedListView.getRecyclerView().addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(
RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
mIdle = (newState == RecyclerView.SCROLL_STATE_IDLE
// Treat dragging as idle, or Espresso will block itself when
// swiping.
|| newState == RecyclerView.SCROLL_STATE_DRAGGING);
if (mIdle && mResourceCallback != null) {
mResourceCallback.onTransitionToIdle();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
}
});
}
@Override
public String getName() {
return PagedListViewScrollingIdlingResource.class.getName();
}
@Override
public boolean isIdleNow() {
return mIdle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
mResourceCallback = callback;
}
}
/**
* Returns a matcher that matches {@link View}s that have the given top margin.
*
* @param topMargin The top margin value to match to.
*/
@NonNull
public static Matcher<View> withTopMargin(int topMargin) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("with top margin: " + topMargin);
}
@Override
public boolean matchesSafely(View view) {
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams) view.getLayoutParams();
return topMargin == params.topMargin;
}
};
}
/**
* Returns a matcher that matches {@link View}s that have the given width.
*
* @param width The width to match to.
*/
@NonNull
public static Matcher<View> withWidth(int width) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("with width: " + width);
}
@Override
public boolean matchesSafely(View view) {
return width == view.getLayoutParams().width;
}
};
}
}