Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.car.widget;
     18 
     19 import static android.support.test.espresso.Espresso.onView;
     20 import static android.support.test.espresso.action.ViewActions.click;
     21 import static android.support.test.espresso.action.ViewActions.swipeDown;
     22 import static android.support.test.espresso.action.ViewActions.swipeUp;
     23 import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
     24 import static android.support.test.espresso.assertion.ViewAssertions.matches;
     25 import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
     26 import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
     27 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
     28 import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
     29 import static android.support.test.espresso.matcher.ViewMatchers.withId;
     30 import static android.support.test.espresso.matcher.ViewMatchers.withText;
     31 
     32 import static org.hamcrest.Matchers.equalTo;
     33 import static org.hamcrest.Matchers.greaterThan;
     34 import static org.hamcrest.Matchers.is;
     35 import static org.hamcrest.Matchers.lessThan;
     36 import static org.hamcrest.Matchers.not;
     37 import static org.junit.Assert.assertEquals;
     38 import static org.junit.Assert.assertThat;
     39 
     40 import android.content.pm.PackageManager;
     41 import android.content.res.Resources;
     42 import android.graphics.drawable.Drawable;
     43 import android.support.test.InstrumentationRegistry;
     44 import android.support.test.annotation.UiThreadTest;
     45 import android.support.test.espresso.Espresso;
     46 import android.support.test.espresso.IdlingResource;
     47 import android.support.test.espresso.matcher.ViewMatchers;
     48 import android.support.test.filters.MediumTest;
     49 import android.support.test.rule.ActivityTestRule;
     50 import android.support.test.runner.AndroidJUnit4;
     51 import android.view.LayoutInflater;
     52 import android.view.View;
     53 import android.view.ViewGroup;
     54 import android.widget.ImageView;
     55 import android.widget.TextView;
     56 
     57 import androidx.annotation.NonNull;
     58 import androidx.car.test.R;
     59 import androidx.recyclerview.widget.GridLayoutManager;
     60 import androidx.recyclerview.widget.LinearLayoutManager;
     61 import androidx.recyclerview.widget.OrientationHelper;
     62 import androidx.recyclerview.widget.RecyclerView;
     63 
     64 import org.hamcrest.Description;
     65 import org.hamcrest.Matcher;
     66 import org.hamcrest.TypeSafeMatcher;
     67 import org.junit.After;
     68 import org.junit.Assume;
     69 import org.junit.Before;
     70 import org.junit.Rule;
     71 import org.junit.Test;
     72 import org.junit.runner.RunWith;
     73 
     74 import java.util.ArrayList;
     75 import java.util.List;
     76 
     77 /** Unit tests for {@link PagedListView}. */
     78 @RunWith(AndroidJUnit4.class)
     79 @MediumTest
     80 public final class PagedListViewTest {
     81 
     82     /**
     83      * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
     84      * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
     85      * to ITEMS_PER_PAGE * desired_pages.
     86      * Actual value does not matter.
     87      */
     88     private static final int ITEMS_PER_PAGE = 5;
     89 
     90     // For tests using GridLayoutManager - assuming each item takes one span, this is essentially
     91     // number of items per row.
     92     private static final int SPAN_COUNT = 5;
     93 
     94     @Rule
     95     public ActivityTestRule<PagedListViewTestActivity> mActivityRule =
     96             new ActivityTestRule<>(PagedListViewTestActivity.class);
     97 
     98     private PagedListViewTestActivity mActivity;
     99     private PagedListView mPagedListView;
    100     private ViewGroup.MarginLayoutParams mRecyclerViewLayoutParams;
    101     private LinearLayoutManager mRecyclerViewLayoutManager;
    102 
    103     /** Returns {@code true} if the testing device has the automotive feature flag. */
    104     private boolean isAutoDevice() {
    105         PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
    106         return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
    107     }
    108 
    109     @Before
    110     public void setUp() {
    111         Assume.assumeTrue(isAutoDevice());
    112 
    113         mActivity = mActivityRule.getActivity();
    114         mPagedListView = mActivity.findViewById(R.id.paged_list_view);
    115         mRecyclerViewLayoutParams =
    116                 (ViewGroup.MarginLayoutParams) mPagedListView.getRecyclerView().getLayoutParams();
    117         mRecyclerViewLayoutManager =
    118                 (LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager();
    119 
    120         // Using deprecated Espresso methods instead of calling it on the IdlingRegistry because
    121         // the latter does not seem to work as reliably. Specifically, on the latter, it does
    122         // not always register and unregister.
    123         Espresso.registerIdlingResources(new PagedListViewScrollingIdlingResource(mPagedListView));
    124     }
    125 
    126     @After
    127     public void tearDown() {
    128         for (IdlingResource idlingResource : Espresso.getIdlingResources()) {
    129             Espresso.unregisterIdlingResources(idlingResource);
    130         }
    131     }
    132 
    133     /** Sets up {@link #mPagedListView} with the given number of items. */
    134     private void setUpPagedListView(int itemCount) {
    135         try {
    136             mActivityRule.runOnUiThread(() -> {
    137                 mPagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
    138                 mPagedListView.setAdapter(
    139                         new TestAdapter(itemCount, mPagedListView.getMeasuredHeight()));
    140             });
    141         } catch (Throwable throwable) {
    142             throwable.printStackTrace();
    143             throw new RuntimeException(throwable);
    144         }
    145     }
    146 
    147     /** Sets up {@link #mPagedListView} with given items. */
    148     private void setupPagedListView(List<ListItem> items) {
    149         try {
    150             mActivityRule.runOnUiThread(() -> {
    151                 mPagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
    152                 mPagedListView.setAdapter(new ListItemAdapter(mActivity,
    153                         new ListItemProvider.ListProvider(items)));
    154             });
    155         } catch (Throwable throwable) {
    156             throwable.printStackTrace();
    157             throw new RuntimeException(throwable);
    158         }
    159     }
    160 
    161     @Test
    162     public void testScrollBarIsInvisibleIfItemsDoNotFillOnePage() {
    163         setUpPagedListView(1 /* itemCount */);
    164         onView(withId(R.id.paged_scroll_view)).check(matches(not(isDisplayed())));
    165     }
    166 
    167     @Test
    168     public void testPageUpButtonDisabledAtTop() {
    169         int itemCount = ITEMS_PER_PAGE * 3;
    170         setUpPagedListView(itemCount);
    171 
    172         // Initially page_up button is disabled.
    173         onView(withId(R.id.page_up)).check(matches(not(isEnabled())));
    174 
    175         // Moving down, should enable the up bottom.
    176         onView(withId(R.id.page_down)).perform(click());
    177         onView(withId(R.id.page_up)).check(matches(isEnabled()));
    178 
    179         // Move back up; this should disable the up bottom again.
    180         onView(withId(R.id.page_up)).perform(click())
    181                 .check(matches(not(isEnabled())));
    182     }
    183 
    184     @Test
    185     public void testItemSnappedToTopOfListOnScroll() throws InterruptedException {
    186         // 2.5 so last page is not full
    187         setUpPagedListView((int) (ITEMS_PER_PAGE * 2.5 /* itemCount */));
    188 
    189         // Going down one page and first item is snapped to top
    190         onView(withId(R.id.page_down)).perform(click());
    191         verifyItemSnappedToListTop();
    192     }
    193 
    194     @Test
    195     public void testLastItemSnappedWhenBottomReached() {
    196         // 2.5 so last page is not full
    197         setUpPagedListView((int) (ITEMS_PER_PAGE * 2.5 /* itemCount */));
    198 
    199         // Go down 2 pages so the bottom is reached.
    200         onView(withId(R.id.page_down)).perform(click());
    201         onView(withId(R.id.page_down)).perform(click()).check(matches(not(isEnabled())));
    202 
    203         // Check that the last item is completely visible.
    204         assertEquals(mRecyclerViewLayoutManager.findLastCompletelyVisibleItemPosition(),
    205                 mRecyclerViewLayoutManager.getItemCount() - 1);
    206     }
    207 
    208     @Test
    209     public void testSwipeDownKeepsItemSnappedToTopOfList() {
    210         setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
    211 
    212         // Go down one page, then swipe down (going up).
    213         onView(withId(R.id.recycler_view)).perform(scrollToPosition(ITEMS_PER_PAGE));
    214         onView(withId(R.id.recycler_view))
    215                 .perform(actionOnItemAtPosition(ITEMS_PER_PAGE, swipeDown()));
    216 
    217         verifyItemSnappedToListTop();
    218     }
    219 
    220     @Test
    221     public void testSwipeUpKeepsItemSnappedToTopOfList() {
    222         setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
    223 
    224         // Swipe up (going down).
    225         onView(withId(R.id.recycler_view))
    226                 .perform(actionOnItemAtPosition(ITEMS_PER_PAGE, swipeUp()));
    227 
    228         verifyItemSnappedToListTop();
    229     }
    230 
    231     @Test
    232     public void testPageUpAndDownMoveSameDistance() {
    233         setUpPagedListView(ITEMS_PER_PAGE * 10);
    234 
    235         // Move down one page so there will be sufficient pages for up and downs.
    236         onView(withId(R.id.page_down)).perform(click());
    237 
    238         int topPosition = mRecyclerViewLayoutManager.findFirstVisibleItemPosition();
    239 
    240         for (int i = 0; i < 3; i++) {
    241             onView(withId(R.id.page_down)).perform(click());
    242             onView(withId(R.id.page_up)).perform(click());
    243         }
    244 
    245         assertThat(mRecyclerViewLayoutManager.findFirstVisibleItemPosition(),
    246                 is(equalTo(topPosition)));
    247     }
    248 
    249     @Test
    250     public void setItemSpacing() throws Throwable {
    251         final int itemCount = 3;
    252         setUpPagedListView(itemCount /* itemCount */);
    253 
    254         // Initial spacing is 0.
    255         final View[] views = new View[itemCount];
    256         mActivityRule.runOnUiThread(() -> {
    257             for (int i = 0; i < mRecyclerViewLayoutManager.getChildCount(); i++) {
    258                 views[i] = mRecyclerViewLayoutManager.getChildAt(i);
    259             }
    260         });
    261         for (int i = 0; i < itemCount - 1; i++) {
    262             assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
    263         }
    264 
    265         // Setting item spacing causes layout change.
    266         // Implicitly wait for layout by making two calls in UI thread.
    267         final int itemSpacing = 10;
    268         mActivityRule.runOnUiThread(() -> {
    269             mPagedListView.setItemSpacing(itemSpacing);
    270         });
    271         mActivityRule.runOnUiThread(() -> {
    272             for (int i = 0; i < mRecyclerViewLayoutManager.getChildCount(); i++) {
    273                 views[i] = mRecyclerViewLayoutManager.getChildAt(i);
    274             }
    275         });
    276         for (int i = 0; i < itemCount - 1; i++) {
    277             assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(itemSpacing)));
    278         }
    279 
    280         // Re-setting spacing back to 0 also works.
    281         mActivityRule.runOnUiThread(() -> {
    282             mPagedListView.setItemSpacing(0);
    283         });
    284         mActivityRule.runOnUiThread(() -> {
    285             for (int i = 0; i < mRecyclerViewLayoutManager.getChildCount(); i++) {
    286                 views[i] = mRecyclerViewLayoutManager.getChildAt(i);
    287             }
    288         });
    289         for (int i = 0; i < itemCount - 1; i++) {
    290             assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
    291         }
    292     }
    293 
    294     @Test
    295     @UiThreadTest
    296     public void testSetScrollBarButtonIcons() throws Throwable {
    297         // Set up a pagedListView with a large item count to ensure the scroll bar buttons are
    298         // always showing.
    299         setUpPagedListView(100 /* itemCount */);
    300 
    301         Drawable upDrawable = mActivity.getDrawable(R.drawable.ic_thumb_up);
    302         mPagedListView.setUpButtonIcon(upDrawable);
    303 
    304         ImageView upButton = mPagedListView.findViewById(R.id.page_up);
    305         ViewMatchers.assertThat(upButton.getDrawable().getConstantState(),
    306                 is(equalTo(upDrawable.getConstantState())));
    307 
    308         Drawable downDrawable = mActivity.getDrawable(R.drawable.ic_thumb_down);
    309         mPagedListView.setDownButtonIcon(downDrawable);
    310 
    311         ImageView downButton = mPagedListView.findViewById(R.id.page_down);
    312         ViewMatchers.assertThat(downButton.getDrawable().getConstantState(),
    313                 is(equalTo(downDrawable.getConstantState())));
    314     }
    315 
    316     @Test
    317     public void testSettingAndResettingScrollbarColor() {
    318         setUpPagedListView(0);
    319 
    320         final int color = R.color.car_teal_700;
    321 
    322         // Setting non-zero res ID changes color.
    323         mPagedListView.setScrollbarColor(color);
    324         assertThat(mPagedListView.mScrollBarView.getScrollbarThumbColor(),
    325                 is(equalTo(InstrumentationRegistry.getContext().getColor(color))));
    326 
    327         // Resets to default color.
    328         mPagedListView.resetScrollbarColor();
    329         assertThat(mPagedListView.mScrollBarView.getScrollbarThumbColor(),
    330                 is(equalTo(InstrumentationRegistry.getContext().getColor(
    331                         R.color.car_scrollbar_thumb))));
    332     }
    333 
    334     @Test
    335     public void testSettingScrollbarColorIgnoresDayNightStyle() {
    336         setUpPagedListView(0);
    337 
    338         final int color = R.color.car_teal_700;
    339         mPagedListView.setScrollbarColor(color);
    340 
    341         int[] styles = new int[] {
    342                 DayNightStyle.AUTO,
    343                 DayNightStyle.AUTO_INVERSE,
    344                 DayNightStyle.ALWAYS_LIGHT,
    345                 DayNightStyle.ALWAYS_DARK,
    346                 DayNightStyle.FORCE_DAY,
    347                 DayNightStyle.FORCE_NIGHT,
    348         };
    349 
    350         for (int style : styles) {
    351             mPagedListView.setDayNightStyle(style);
    352 
    353             assertThat(mPagedListView.mScrollBarView.getScrollbarThumbColor(),
    354                     is(equalTo(InstrumentationRegistry.getContext().getColor(color))));
    355         }
    356     }
    357 
    358     @Test
    359     public void testNoVerticalPaddingOnScrollBar() {
    360         // Just need enough items to ensure the scroll bar is showing.
    361         setUpPagedListView(ITEMS_PER_PAGE * 10);
    362         onView(withId(R.id.paged_scroll_view))
    363                 .check(matches(withTopPadding(0)))
    364                 .check(matches(withBottomPadding(0)));
    365     }
    366 
    367     @Test
    368     public void testDefaultScrollBarTopMargin() {
    369         Resources res = InstrumentationRegistry.getContext().getResources();
    370         int defaultTopMargin = res.getDimensionPixelSize(R.dimen.car_padding_4);
    371 
    372         // Just need enough items to ensure the scroll bar is showing.
    373         setUpPagedListView(ITEMS_PER_PAGE * 10);
    374         onView(withId(R.id.paged_scroll_view)).check(matches(withTopMargin(defaultTopMargin)));
    375     }
    376 
    377     @Test
    378     public void testSetScrollbarTopMargin() {
    379         // Just need enough items to ensure the scroll bar is showing.
    380         setUpPagedListView(ITEMS_PER_PAGE * 10);
    381 
    382         int topMargin = 100;
    383         mPagedListView.setScrollBarTopMargin(topMargin);
    384 
    385         onView(withId(R.id.paged_scroll_view)).check(matches(withTopMargin(topMargin)));
    386     }
    387 
    388     @Test
    389     public void testSetGutterNone() {
    390         // Just need enough items to ensure the scroll bar is showing.
    391         setUpPagedListView(ITEMS_PER_PAGE * 10);
    392 
    393         mPagedListView.setGutter(PagedListView.Gutter.NONE);
    394 
    395         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(0)));
    396         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(0)));
    397     }
    398 
    399     @Test
    400     public void testSetGutterStart() {
    401         // Just need enough items to ensure the scroll bar is showing.
    402         setUpPagedListView(ITEMS_PER_PAGE * 10);
    403 
    404         mPagedListView.setGutter(PagedListView.Gutter.START);
    405 
    406         Resources res = InstrumentationRegistry.getContext().getResources();
    407         int gutterSize = res.getDimensionPixelSize(R.dimen.car_margin);
    408 
    409         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(gutterSize)));
    410         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(0)));
    411     }
    412 
    413     @Test
    414     public void testSetGutterEnd() {
    415         // Just need enough items to ensure the scroll bar is showing.
    416         setUpPagedListView(ITEMS_PER_PAGE * 10);
    417 
    418         mPagedListView.setGutter(PagedListView.Gutter.END);
    419 
    420         Resources res = InstrumentationRegistry.getContext().getResources();
    421         int gutterSize = res.getDimensionPixelSize(R.dimen.car_margin);
    422 
    423         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(0)));
    424         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(gutterSize)));
    425     }
    426 
    427     @Test
    428     public void testSetGutterBoth() {
    429         // Just need enough items to ensure the scroll bar is showing.
    430         setUpPagedListView(ITEMS_PER_PAGE * 10);
    431 
    432         mPagedListView.setGutter(PagedListView.Gutter.BOTH);
    433 
    434         Resources res = InstrumentationRegistry.getContext().getResources();
    435         int gutterSize = res.getDimensionPixelSize(R.dimen.car_margin);
    436 
    437         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(gutterSize)));
    438         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(gutterSize)));
    439     }
    440 
    441     @Test
    442     public void testSetGutterSizeNone() {
    443         // Just need enough items to ensure the scroll bar is showing.
    444         setUpPagedListView(ITEMS_PER_PAGE * 10);
    445 
    446         mPagedListView.setGutter(PagedListView.Gutter.NONE);
    447         mPagedListView.setGutterSize(120);
    448 
    449         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(0)));
    450         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(0)));
    451     }
    452 
    453     @Test
    454     public void testSetGutterSizeStart() {
    455         // Just need enough items to ensure the scroll bar is showing.
    456         setUpPagedListView(ITEMS_PER_PAGE * 10);
    457 
    458         mPagedListView.setGutter(PagedListView.Gutter.START);
    459 
    460         int gutterSize = 120;
    461         mPagedListView.setGutterSize(gutterSize);
    462 
    463         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(gutterSize)));
    464         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(0)));
    465     }
    466 
    467     @Test
    468     public void testSetGutterSizeEnd() {
    469         // Just need enough items to ensure the scroll bar is showing.
    470         setUpPagedListView(ITEMS_PER_PAGE * 10);
    471 
    472         mPagedListView.setGutter(PagedListView.Gutter.END);
    473 
    474         int gutterSize = 120;
    475         mPagedListView.setGutterSize(gutterSize);
    476 
    477         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(0)));
    478         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(gutterSize)));
    479     }
    480 
    481     @Test
    482     public void testSetGutterSizeBoth() {
    483         // Just need enough items to ensure the scroll bar is showing.
    484         setUpPagedListView(ITEMS_PER_PAGE * 10);
    485 
    486         mPagedListView.setGutter(PagedListView.Gutter.BOTH);
    487 
    488         int gutterSize = 120;
    489         mPagedListView.setGutterSize(gutterSize);
    490 
    491         assertThat(mRecyclerViewLayoutParams.getMarginStart(), is(equalTo(gutterSize)));
    492         assertThat(mRecyclerViewLayoutParams.getMarginEnd(), is(equalTo(gutterSize)));
    493     }
    494 
    495     @Test
    496     public void setDefaultScrollBarContainerWidth() {
    497         // Just need enough items to ensure the scroll bar is showing.
    498         setUpPagedListView(ITEMS_PER_PAGE * 10);
    499 
    500         Resources res = InstrumentationRegistry.getContext().getResources();
    501         int defaultWidth = res.getDimensionPixelSize(R.dimen.car_margin);
    502 
    503         onView(withId(R.id.paged_scroll_view)).check(matches(withWidth(defaultWidth)));
    504     }
    505 
    506     @Test
    507     public void testSetScrollBarContainerWidth() {
    508         // Just need enough items to ensure the scroll bar is showing.
    509         setUpPagedListView(ITEMS_PER_PAGE * 10);
    510 
    511         int scrollBarContainerWidth = 120;
    512         mPagedListView.setScrollBarContainerWidth(scrollBarContainerWidth);
    513 
    514         onView(withId(R.id.paged_scroll_view)).check(matches(withWidth(scrollBarContainerWidth)));
    515     }
    516 
    517     @Test
    518     public void testTopOffsetInGridLayoutManager() throws Throwable {
    519         int topOffset = mActivity.getResources().getDimensionPixelSize(R.dimen.car_padding_5);
    520 
    521         // Need enough items to fill the first row.
    522         setUpPagedListView(SPAN_COUNT * 3);
    523         mActivityRule.runOnUiThread(() -> {
    524             mPagedListView.setListContentTopOffset(topOffset);
    525             mPagedListView.getRecyclerView().setLayoutManager(
    526                     new GridLayoutManager(mActivity, SPAN_COUNT));
    527             // Verify only items in first row have top offset. Setting no item spacing to avoid
    528             // additional offset.
    529             mPagedListView.setItemSpacing(0);
    530         });
    531         // Wait for paged list view to layout by using espresso to scroll to a position.
    532         onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
    533 
    534         for (int i = 0; i < SPAN_COUNT; i++) {
    535             assertThat(mPagedListView.getRecyclerView().getChildAt(i).getTop(),
    536                     is(equalTo(topOffset)));
    537 
    538             // i + SPAN_COUNT uses items in second row.
    539             assertThat(mPagedListView.getRecyclerView().getChildAt(i + SPAN_COUNT).getTop(),
    540                     is(equalTo(mPagedListView.getRecyclerView().getChildAt(i).getBottom())));
    541         }
    542     }
    543 
    544     @Test
    545     public void testPageDownScrollsOverLongItem() throws Throwable {
    546         // Verifies that page down button gradually steps over item longer than parent size.
    547         TextListItem item;
    548         List<ListItem> items = new ArrayList<>();
    549 
    550         // Need enough items on both ends of long item so long item is not immediately shown.
    551         int fillerItemCount = ITEMS_PER_PAGE * 6;
    552         for (int i = 0; i < fillerItemCount; i++) {
    553             item = new TextListItem(mActivity);
    554             item.setTitle("title " + i);
    555             items.add(item);
    556         }
    557 
    558         int longItemPos = fillerItemCount / 2;
    559         item = new TextListItem(mActivity);
    560         item.setBody(mActivity.getResources().getString(R.string.longer_than_screen_size));
    561         items.add(longItemPos, item);
    562 
    563         item = new TextListItem(mActivity);
    564         item.setTitle("title add item after long item");
    565         items.add(item);
    566 
    567         setupPagedListView(items);
    568 
    569         OrientationHelper orientationHelper = OrientationHelper.createVerticalHelper(
    570                 mPagedListView.getRecyclerView().getLayoutManager());
    571 
    572         // Scroll to a position where long item is partially visible.
    573         // Scrolling from top, scrollToPosition() either aligns the pos-1 item to bottom,
    574         // or scrolls to the center of long item. So we hack a bit by scrolling the distance of one
    575         // item height over pos-1 item.
    576         onView(withId(R.id.recycler_view)).perform(scrollToPosition(longItemPos - 1));
    577         // Scroll by the height of an item so the long item is partially visible.
    578         mActivityRule.runOnUiThread(() -> mPagedListView.getRecyclerView().scrollBy(0,
    579                 mPagedListView.getRecyclerView().getChildAt(0).getHeight()));
    580 
    581         // Verify long item is partially shown.
    582         View longItem = findLongItem();
    583         assertThat(orientationHelper.getDecoratedStart(longItem),
    584                 is(greaterThan(mPagedListView.getRecyclerView().getTop())));
    585 
    586         onView(withId(R.id.page_down)).perform(click());
    587 
    588         // Verify long item is snapped to top.
    589         assertThat(orientationHelper.getDecoratedStart(longItem), is(equalTo(0)));
    590         assertThat(orientationHelper.getDecoratedEnd(longItem),
    591                 is(greaterThan(mPagedListView.getBottom())));
    592 
    593         // Set a limit to avoid test stuck in non-moving state.
    594         int limit = 10;
    595         for (int pageCount = 0; pageCount < limit
    596                 && orientationHelper.getDecoratedEnd(longItem)
    597                 > mPagedListView.getRecyclerView().getBottom();
    598                 pageCount++) {
    599             onView(withId(R.id.page_down)).perform(click());
    600         }
    601         // Verify long item end is aligned to bottom.
    602         assertThat(orientationHelper.getDecoratedEnd(longItem),
    603                 is(equalTo(mPagedListView.getHeight())));
    604 
    605         onView(withId(R.id.page_down)).perform(click());
    606         // Verify that the long item is no longer visible; Should be on the next child
    607         assertThat(orientationHelper.getDecoratedStart(longItem),
    608                 is(lessThan(mPagedListView.getRecyclerView().getTop())));
    609     }
    610 
    611     @Test
    612     public void testPageUpScrollsOverLongItem() throws Throwable {
    613         // Verifies that page down button gradually steps over item longer than parent size.
    614         TextListItem item;
    615         List<ListItem> items = new ArrayList<>();
    616 
    617         // Need enough items on both ends of long item so long item is not immediately shown.
    618         int fillerItemCount = ITEMS_PER_PAGE * 6;
    619         for (int i = 0; i < fillerItemCount; i++) {
    620             item = new TextListItem(mActivity);
    621             item.setTitle("title " + i);
    622             items.add(item);
    623         }
    624 
    625         int longItemPos = fillerItemCount / 2;
    626         item = new TextListItem(mActivity);
    627         item.setBody(mActivity.getResources().getString(R.string.longer_than_screen_size));
    628         items.add(longItemPos, item);
    629 
    630         setupPagedListView(items);
    631 
    632         OrientationHelper orientationHelper = OrientationHelper.createVerticalHelper(
    633                 mPagedListView.getRecyclerView().getLayoutManager());
    634 
    635         // Scroll to a position where long item is partially shown.
    636         onView(withId(R.id.recycler_view)).perform(scrollToPosition(longItemPos + 1));
    637 
    638         // Verify long item is partially shown.
    639         View longItem = findLongItem();
    640         assertThat(orientationHelper.getDecoratedEnd(longItem),
    641                 is(greaterThan(mPagedListView.getRecyclerView().getTop())));
    642 
    643         onView(withId(R.id.page_up)).perform(click());
    644 
    645         // Verify long item is snapped to bottom.
    646         assertThat(orientationHelper.getDecoratedEnd(longItem),
    647                 is(equalTo(mPagedListView.getHeight())));
    648         assertThat(orientationHelper.getDecoratedStart(longItem), is(lessThan(0)));
    649 
    650         // Set a limit to avoid test stuck in non-moving state.
    651         int limit = 10;
    652         for (int pageCount = 0; pageCount < limit
    653                 && orientationHelper.getDecoratedStart(longItem) < 0;
    654                 pageCount++) {
    655             onView(withId(R.id.page_up)).perform(click());
    656         }
    657         // Verify long item top is aligned to top.
    658         assertThat(orientationHelper.getDecoratedStart(longItem), is(equalTo(0)));
    659     }
    660 
    661     private View findLongItem() {
    662         for (int i = 0; i < mPagedListView.getRecyclerView().getChildCount(); i++) {
    663             View item = mPagedListView.getRecyclerView().getChildAt(i);
    664             if (item.getHeight() > mPagedListView.getHeight()) {
    665                 return item;
    666             }
    667         }
    668         return null;
    669     }
    670 
    671     private static String itemText(int index) {
    672         return "Data " + index;
    673     }
    674 
    675     /**
    676      * Checks that the first item in the list is completely shown and no part of a previous item
    677      * is shown.
    678      */
    679     private void verifyItemSnappedToListTop() {
    680         int firstVisiblePosition =
    681                 mRecyclerViewLayoutManager.findFirstCompletelyVisibleItemPosition();
    682         if (firstVisiblePosition > 1) {
    683             int lastInPreviousPagePosition = firstVisiblePosition - 1;
    684             onView(withText(itemText(lastInPreviousPagePosition)))
    685                     .check(doesNotExist());
    686         }
    687     }
    688 
    689     /** A base adapter that will handle inflating the test view and binding data to it. */
    690     private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
    691         private List<String> mData;
    692         private int mParentHeight;
    693 
    694         TestAdapter(int itemCount, int parentHeight) {
    695             mData = new ArrayList<>();
    696             for (int i = 0; i < itemCount; i++) {
    697                 mData.add(itemText(i));
    698             }
    699             mParentHeight = parentHeight;
    700         }
    701 
    702         @Override
    703         public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    704             LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    705             return new TestViewHolder(inflater, parent);
    706         }
    707 
    708         @Override
    709         public void onBindViewHolder(TestViewHolder holder, int position) {
    710             // Calculate height for an item so one page fits ITEMS_PER_PAGE items.
    711             int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
    712             holder.itemView.setMinimumHeight(height);
    713             holder.bind(mData.get(position));
    714         }
    715 
    716         @Override
    717         public int getItemCount() {
    718             return mData.size();
    719         }
    720     }
    721 
    722     private class TestViewHolder extends RecyclerView.ViewHolder {
    723         private TextView mTextView;
    724 
    725         TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
    726             super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
    727             mTextView = itemView.findViewById(R.id.text_view);
    728         }
    729 
    730         public void bind(String text) {
    731             mTextView.setText(text);
    732         }
    733     }
    734 
    735     /**
    736      * An {@link IdlingResource} that will prevent assertions from running while the
    737      * {@link #mPagedListView} is scrolling.
    738      */
    739     private class PagedListViewScrollingIdlingResource implements IdlingResource {
    740         private boolean mIdle = true;
    741         private ResourceCallback mResourceCallback;
    742 
    743         PagedListViewScrollingIdlingResource(PagedListView pagedListView) {
    744             pagedListView.getRecyclerView().addOnScrollListener(
    745                     new RecyclerView.OnScrollListener() {
    746                         @Override
    747                         public void onScrollStateChanged(
    748                                 RecyclerView recyclerView, int newState) {
    749                             super.onScrollStateChanged(recyclerView, newState);
    750                             mIdle = (newState == RecyclerView.SCROLL_STATE_IDLE
    751                                     // Treat dragging as idle, or Espresso will block itself when
    752                                     // swiping.
    753                                     || newState == RecyclerView.SCROLL_STATE_DRAGGING);
    754                             if (mIdle && mResourceCallback != null) {
    755                                 mResourceCallback.onTransitionToIdle();
    756                             }
    757                         }
    758 
    759                         @Override
    760                         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    761                         }
    762                     });
    763         }
    764 
    765         @Override
    766         public String getName() {
    767             return PagedListViewScrollingIdlingResource.class.getName();
    768         }
    769 
    770         @Override
    771         public boolean isIdleNow() {
    772             return mIdle;
    773         }
    774 
    775         @Override
    776         public void registerIdleTransitionCallback(ResourceCallback callback) {
    777             mResourceCallback = callback;
    778         }
    779     }
    780 
    781     /**
    782      * Returns a matcher that matches {@link View}s that have the given top margin.
    783      *
    784      * @param topMargin The top margin value to match to.
    785      */
    786     @NonNull
    787     public static Matcher<View> withTopMargin(int topMargin) {
    788         return new TypeSafeMatcher<View>() {
    789             @Override
    790             public void describeTo(Description description) {
    791                 description.appendText("with top margin: " + topMargin);
    792             }
    793 
    794             @Override
    795             public boolean matchesSafely(View view) {
    796                 ViewGroup.MarginLayoutParams params =
    797                         (ViewGroup.MarginLayoutParams) view.getLayoutParams();
    798                 return topMargin == params.topMargin;
    799             }
    800         };
    801     }
    802 
    803     /**
    804      * Returns a matcher that matches {@link View}s that have the given top padding.
    805      *
    806      * @param topPadding The top padding value to match to.
    807      */
    808     @NonNull
    809     public static Matcher<View> withTopPadding(int topPadding) {
    810         return new TypeSafeMatcher<View>() {
    811             @Override
    812             public void describeTo(Description description) {
    813                 description.appendText("with top padding: " + topPadding);
    814             }
    815 
    816             @Override
    817             public boolean matchesSafely(View view) {
    818                 return topPadding == view.getPaddingTop();
    819             }
    820         };
    821     }
    822 
    823     /**
    824      * Returns a matcher that matches {@link View}s that have the given bottom padding.
    825      *
    826      * @param bottomPadding The bottom padding value to match to.
    827      */
    828     @NonNull
    829     public static Matcher<View> withBottomPadding(int bottomPadding) {
    830         return new TypeSafeMatcher<View>() {
    831             @Override
    832             public void describeTo(Description description) {
    833                 description.appendText("with bottom padding: " + bottomPadding);
    834             }
    835 
    836             @Override
    837             public boolean matchesSafely(View view) {
    838                 return bottomPadding == view.getPaddingBottom();
    839             }
    840         };
    841     }
    842 
    843     /**
    844      * Returns a matcher that matches {@link View}s that have the given width.
    845      *
    846      * @param width The width to match to.
    847      */
    848     @NonNull
    849     public static Matcher<View> withWidth(int width) {
    850         return new TypeSafeMatcher<View>() {
    851             @Override
    852             public void describeTo(Description description) {
    853                 description.appendText("with width: " + width);
    854             }
    855 
    856             @Override
    857             public boolean matchesSafely(View view) {
    858                 return width == view.getLayoutParams().width;
    859             }
    860         };
    861     }
    862 }
    863