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