BaseRecyclerViewInstrumentationTest.java revision 9290baf2c13c27d6f6d517b198a3fc079b83f297
1/* 2 * Copyright (C) 2014 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 17package android.support.v7.widget; 18 19import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE; 20 21import org.hamcrest.CoreMatchers; 22import org.hamcrest.MatcherAssert; 23import org.junit.After; 24import org.junit.Before; 25import org.junit.Rule; 26 27import android.app.Instrumentation; 28import android.graphics.Rect; 29import android.os.Looper; 30import android.support.test.InstrumentationRegistry; 31import android.support.test.rule.ActivityTestRule; 32import android.support.v4.view.ViewCompat; 33import android.support.v7.recyclerview.test.SameActivityTestRule; 34import android.util.Log; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.FrameLayout; 39import android.widget.TextView; 40 41import java.lang.reflect.InvocationTargetException; 42import java.lang.reflect.Method; 43import java.util.ArrayList; 44import java.util.HashSet; 45import java.util.List; 46import java.util.Set; 47import java.util.concurrent.CountDownLatch; 48import java.util.concurrent.TimeUnit; 49import java.util.concurrent.atomic.AtomicBoolean; 50import java.util.concurrent.atomic.AtomicInteger; 51import java.util.concurrent.locks.ReentrantLock; 52import android.support.v7.recyclerview.test.R; 53 54import static org.junit.Assert.*; 55 56import static java.util.concurrent.TimeUnit.SECONDS; 57 58abstract public class BaseRecyclerViewInstrumentationTest { 59 60 private static final String TAG = "RecyclerViewTest"; 61 62 private boolean mDebug; 63 64 protected RecyclerView mRecyclerView; 65 66 protected AdapterHelper mAdapterHelper; 67 68 private Throwable mMainThreadException; 69 70 private boolean mIgnoreMainThreadException = false; 71 72 Thread mInstrumentationThread; 73 74 @Rule 75 public ActivityTestRule<TestActivity> mActivityRule = new SameActivityTestRule() { 76 @Override 77 public boolean canReUseActivity() { 78 return BaseRecyclerViewInstrumentationTest.this.canReUseActivity(); 79 } 80 }; 81 82 public BaseRecyclerViewInstrumentationTest() { 83 this(false); 84 } 85 86 public BaseRecyclerViewInstrumentationTest(boolean debug) { 87 mDebug = debug; 88 } 89 90 void checkForMainThreadException() throws Throwable { 91 if (!mIgnoreMainThreadException && mMainThreadException != null) { 92 throw mMainThreadException; 93 } 94 } 95 96 public void setIgnoreMainThreadException(boolean ignoreMainThreadException) { 97 mIgnoreMainThreadException = ignoreMainThreadException; 98 } 99 100 public Throwable getMainThreadException() { 101 return mMainThreadException; 102 } 103 104 protected TestActivity getActivity() { 105 return mActivityRule.getActivity(); 106 } 107 108 @Before 109 public final void setUpInsThread() throws Exception { 110 mInstrumentationThread = Thread.currentThread(); 111 Item.idCounter.set(0); 112 } 113 114 void setHasTransientState(final View view, final boolean value) { 115 try { 116 runTestOnUiThread(new Runnable() { 117 @Override 118 public void run() { 119 ViewCompat.setHasTransientState(view, value); 120 } 121 }); 122 } catch (Throwable throwable) { 123 Log.e(TAG, "", throwable); 124 } 125 } 126 127 public boolean canReUseActivity() { 128 return true; 129 } 130 131 protected void enableAccessibility() 132 throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 133 Method getUIAutomation = Instrumentation.class.getMethod("getUiAutomation"); 134 getUIAutomation.invoke(InstrumentationRegistry.getInstrumentation()); 135 } 136 137 void setAdapter(final RecyclerView.Adapter adapter) throws Throwable { 138 runTestOnUiThread(new Runnable() { 139 @Override 140 public void run() { 141 mRecyclerView.setAdapter(adapter); 142 } 143 }); 144 } 145 146 public View focusSearch(final View focused, final int direction) 147 throws Throwable { 148 final View[] result = new View[1]; 149 runTestOnUiThread(new Runnable() { 150 @Override 151 public void run() { 152 View view = focused.focusSearch(direction); 153 if (view != null && view != focused) { 154 view.requestFocus(); 155 } 156 result[0] = view; 157 } 158 }); 159 return result[0]; 160 } 161 162 protected WrappedRecyclerView inflateWrappedRV() { 163 return (WrappedRecyclerView) 164 LayoutInflater.from(getActivity()).inflate(R.layout.wrapped_test_rv, 165 getRecyclerViewContainer(), false); 166 } 167 168 void swapAdapter(final RecyclerView.Adapter adapter, 169 final boolean removeAndRecycleExistingViews) throws Throwable { 170 runTestOnUiThread(new Runnable() { 171 @Override 172 public void run() { 173 try { 174 mRecyclerView.swapAdapter(adapter, removeAndRecycleExistingViews); 175 } catch (Throwable t) { 176 postExceptionToInstrumentation(t); 177 } 178 } 179 }); 180 checkForMainThreadException(); 181 } 182 183 void postExceptionToInstrumentation(Throwable t) { 184 if (mInstrumentationThread == Thread.currentThread()) { 185 throw new RuntimeException(t); 186 } 187 if (mMainThreadException != null) { 188 Log.e(TAG, "receiving another main thread exception. dropping.", t); 189 } else { 190 Log.e(TAG, "captured exception on main thread", t); 191 mMainThreadException = t; 192 } 193 194 if (mRecyclerView != null && mRecyclerView 195 .getLayoutManager() instanceof TestLayoutManager) { 196 TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager(); 197 // finish all layouts so that we get the correct exception 198 if (lm.layoutLatch != null) { 199 while (lm.layoutLatch.getCount() > 0) { 200 lm.layoutLatch.countDown(); 201 } 202 } 203 } 204 } 205 206 public Instrumentation getInstrumentation() { 207 return InstrumentationRegistry.getInstrumentation(); 208 } 209 210 @After 211 public final void tearDown() throws Exception { 212 if (mRecyclerView != null) { 213 try { 214 removeRecyclerView(); 215 } catch (Throwable throwable) { 216 throwable.printStackTrace(); 217 } 218 } 219 getInstrumentation().waitForIdleSync(); 220 221 try { 222 checkForMainThreadException(); 223 } catch (Exception e) { 224 throw e; 225 } catch (Throwable throwable) { 226 throw new Exception(Log.getStackTraceString(throwable)); 227 } 228 } 229 230 public Rect getDecoratedRecyclerViewBounds() { 231 return new Rect( 232 mRecyclerView.getPaddingLeft(), 233 mRecyclerView.getPaddingTop(), 234 mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), 235 mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() 236 ); 237 } 238 239 public void removeRecyclerView() throws Throwable { 240 if (mRecyclerView == null) { 241 return; 242 } 243 if (!isMainThread()) { 244 getInstrumentation().waitForIdleSync(); 245 } 246 runTestOnUiThread(new Runnable() { 247 @Override 248 public void run() { 249 try { 250 // do not run validation if we already have an error 251 if (mMainThreadException == null) { 252 final RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); 253 if (adapter instanceof AttachDetachCountingAdapter) { 254 ((AttachDetachCountingAdapter) adapter).getCounter() 255 .validateRemaining(mRecyclerView); 256 } 257 } 258 getActivity().getContainer().removeAllViews(); 259 } catch (Throwable t) { 260 postExceptionToInstrumentation(t); 261 } 262 } 263 }); 264 mRecyclerView = null; 265 } 266 267 void waitForAnimations(int seconds) throws Throwable { 268 final CountDownLatch latch = new CountDownLatch(1); 269 runTestOnUiThread(new Runnable() { 270 @Override 271 public void run() { 272 mRecyclerView.mItemAnimator 273 .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 274 @Override 275 public void onAnimationsFinished() { 276 latch.countDown(); 277 } 278 }); 279 } 280 }); 281 282 assertTrue("animations didn't finish on expected time of " + seconds + " seconds", 283 latch.await(seconds, TimeUnit.SECONDS)); 284 } 285 286 public void waitForIdleScroll(final RecyclerView recyclerView) throws Throwable { 287 final CountDownLatch latch = new CountDownLatch(1); 288 runTestOnUiThread(new Runnable() { 289 @Override 290 public void run() { 291 RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() { 292 @Override 293 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 294 if (newState == SCROLL_STATE_IDLE) { 295 latch.countDown(); 296 recyclerView.removeOnScrollListener(this); 297 } 298 } 299 }; 300 if (recyclerView.getScrollState() == SCROLL_STATE_IDLE) { 301 latch.countDown(); 302 } else { 303 recyclerView.addOnScrollListener(listener); 304 } 305 } 306 }); 307 assertTrue("should go idle in 10 seconds", latch.await(10, TimeUnit.SECONDS)); 308 } 309 310 public boolean requestFocus(final View view, boolean waitForScroll) throws Throwable { 311 final boolean[] result = new boolean[1]; 312 try { 313 runTestOnUiThread(new Runnable() { 314 @Override 315 public void run() { 316 result[0] = view.requestFocus(); 317 } 318 }); 319 } catch (Throwable throwable) { 320 fail(throwable.getMessage()); 321 } 322 if (waitForScroll && result[0]) { 323 waitForIdleScroll(mRecyclerView); 324 } 325 return result[0]; 326 } 327 328 public void setRecyclerView(final RecyclerView recyclerView) throws Throwable { 329 setRecyclerView(recyclerView, true); 330 } 331 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool) 332 throws Throwable { 333 setRecyclerView(recyclerView, assignDummyPool, true); 334 } 335 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool, 336 boolean addPositionCheckItemAnimator) 337 throws Throwable { 338 mRecyclerView = recyclerView; 339 if (assignDummyPool) { 340 RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 341 @Override 342 public RecyclerView.ViewHolder getRecycledView(int viewType) { 343 RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType); 344 if (viewHolder == null) { 345 return null; 346 } 347 viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND); 348 viewHolder.mPosition = 200; 349 viewHolder.mOldPosition = 300; 350 viewHolder.mPreLayoutPosition = 500; 351 return viewHolder; 352 } 353 354 @Override 355 public void putRecycledView(RecyclerView.ViewHolder scrap) { 356 assertNull(scrap.mOwnerRecyclerView); 357 super.putRecycledView(scrap); 358 } 359 }; 360 mRecyclerView.setRecycledViewPool(pool); 361 } 362 if (addPositionCheckItemAnimator) { 363 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 364 @Override 365 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 366 RecyclerView.State state) { 367 RecyclerView.ViewHolder vh = parent.getChildViewHolder(view); 368 if (!vh.isRemoved()) { 369 assertNotSame("If getItemOffsets is called, child should have a valid" 370 + " adapter position unless it is removed : " + vh, 371 vh.getAdapterPosition(), RecyclerView.NO_POSITION); 372 } 373 } 374 }); 375 } 376 mAdapterHelper = recyclerView.mAdapterHelper; 377 runTestOnUiThread(new Runnable() { 378 @Override 379 public void run() { 380 getActivity().getContainer().addView(recyclerView); 381 } 382 }); 383 } 384 385 protected FrameLayout getRecyclerViewContainer() { 386 return getActivity().getContainer(); 387 } 388 389 public void requestLayoutOnUIThread(final View view) { 390 try { 391 runTestOnUiThread(new Runnable() { 392 @Override 393 public void run() { 394 view.requestLayout(); 395 } 396 }); 397 } catch (Throwable throwable) { 398 Log.e(TAG, "", throwable); 399 } 400 } 401 402 public void scrollBy(final int dt) { 403 try { 404 runTestOnUiThread(new Runnable() { 405 @Override 406 public void run() { 407 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 408 mRecyclerView.scrollBy(dt, 0); 409 } else { 410 mRecyclerView.scrollBy(0, dt); 411 } 412 413 } 414 }); 415 } catch (Throwable throwable) { 416 Log.e(TAG, "", throwable); 417 } 418 } 419 420 public void smoothScrollBy(final int dt) { 421 try { 422 runTestOnUiThread(new Runnable() { 423 @Override 424 public void run() { 425 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 426 mRecyclerView.smoothScrollBy(dt, 0); 427 } else { 428 mRecyclerView.smoothScrollBy(0, dt); 429 } 430 431 } 432 }); 433 } catch (Throwable throwable) { 434 Log.e(TAG, "", throwable); 435 } 436 getInstrumentation().waitForIdleSync(); 437 } 438 439 void scrollToPosition(final int position) throws Throwable { 440 runTestOnUiThread(new Runnable() { 441 @Override 442 public void run() { 443 mRecyclerView.getLayoutManager().scrollToPosition(position); 444 } 445 }); 446 } 447 448 void smoothScrollToPosition(final int position) throws Throwable { 449 smoothScrollToPosition(position, true); 450 } 451 452 void smoothScrollToPosition(final int position, boolean assertArrival) throws Throwable { 453 if (mDebug) { 454 Log.d(TAG, "SMOOTH scrolling to " + position); 455 } 456 final CountDownLatch viewAdded = new CountDownLatch(1); 457 final RecyclerView.OnChildAttachStateChangeListener listener = 458 new RecyclerView.OnChildAttachStateChangeListener() { 459 @Override 460 public void onChildViewAttachedToWindow(View view) { 461 if (position == mRecyclerView.getChildAdapterPosition(view)) { 462 viewAdded.countDown(); 463 } 464 } 465 @Override 466 public void onChildViewDetachedFromWindow(View view) { 467 } 468 }; 469 final AtomicBoolean addedListener = new AtomicBoolean(false); 470 runTestOnUiThread(new Runnable() { 471 @Override 472 public void run() { 473 RecyclerView.ViewHolder viewHolderForAdapterPosition = 474 mRecyclerView.findViewHolderForAdapterPosition(position); 475 if (viewHolderForAdapterPosition != null) { 476 viewAdded.countDown(); 477 } else { 478 mRecyclerView.addOnChildAttachStateChangeListener(listener); 479 addedListener.set(true); 480 } 481 482 } 483 }); 484 runTestOnUiThread(new Runnable() { 485 @Override 486 public void run() { 487 mRecyclerView.smoothScrollToPosition(position); 488 } 489 }); 490 getInstrumentation().waitForIdleSync(); 491 assertThat("should be able to scroll in 10 seconds", !assertArrival || 492 viewAdded.await(10, TimeUnit.SECONDS), 493 CoreMatchers.is(true)); 494 waitForIdleScroll(mRecyclerView); 495 if (mDebug) { 496 Log.d(TAG, "SMOOTH scrolling done"); 497 } 498 if (addedListener.get()) { 499 runTestOnUiThread(new Runnable() { 500 @Override 501 public void run() { 502 mRecyclerView.removeOnChildAttachStateChangeListener(listener); 503 } 504 }); 505 } 506 getInstrumentation().waitForIdleSync(); 507 } 508 509 void freezeLayout(final boolean freeze) throws Throwable { 510 runTestOnUiThread(new Runnable() { 511 @Override 512 public void run() { 513 mRecyclerView.setLayoutFrozen(freeze); 514 } 515 }); 516 } 517 518 public void setVisibility(final View view, final int visibility) throws Throwable { 519 runTestOnUiThread(new Runnable() { 520 @Override 521 public void run() { 522 view.setVisibility(visibility); 523 } 524 }); 525 } 526 527 public class TestViewHolder extends RecyclerView.ViewHolder { 528 529 Item mBoundItem; 530 531 public TestViewHolder(View itemView) { 532 super(itemView); 533 itemView.setFocusable(true); 534 } 535 536 @Override 537 public String toString() { 538 return super.toString() + " item:" + mBoundItem; 539 } 540 } 541 class DumbLayoutManager extends TestLayoutManager { 542 @Override 543 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 544 detachAndScrapAttachedViews(recycler); 545 layoutRange(recycler, 0, state.getItemCount()); 546 if (layoutLatch != null) { 547 layoutLatch.countDown(); 548 } 549 } 550 } 551 public class TestLayoutManager extends RecyclerView.LayoutManager { 552 int mScrollVerticallyAmount; 553 int mScrollHorizontallyAmount; 554 protected CountDownLatch layoutLatch; 555 private boolean mSupportsPredictive = false; 556 557 public void expectLayouts(int count) { 558 layoutLatch = new CountDownLatch(count); 559 } 560 561 public void waitForLayout(int seconds) throws Throwable { 562 layoutLatch.await(seconds * (mDebug ? 1000 : 1), SECONDS); 563 checkForMainThreadException(); 564 MatcherAssert.assertThat("all layouts should complete on time", 565 layoutLatch.getCount(), CoreMatchers.is(0L)); 566 // use a runnable to ensure RV layout is finished 567 getInstrumentation().runOnMainSync(new Runnable() { 568 @Override 569 public void run() { 570 } 571 }); 572 } 573 574 public boolean isSupportsPredictive() { 575 return mSupportsPredictive; 576 } 577 578 public void setSupportsPredictive(boolean supportsPredictive) { 579 mSupportsPredictive = supportsPredictive; 580 } 581 582 @Override 583 public boolean supportsPredictiveItemAnimations() { 584 return mSupportsPredictive; 585 } 586 587 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 588 layoutLatch.await(timeout, TimeUnit.SECONDS); 589 assertEquals(msg, count, layoutLatch.getCount()); 590 } 591 592 public void assertNoLayout(String msg, long timeout) throws Throwable { 593 layoutLatch.await(timeout, TimeUnit.SECONDS); 594 assertFalse(msg, layoutLatch.getCount() == 0); 595 } 596 597 @Override 598 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 599 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 600 ViewGroup.LayoutParams.WRAP_CONTENT); 601 } 602 603 void assertVisibleItemPositions() { 604 int i = getChildCount(); 605 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 606 while (i-- > 0) { 607 View view = getChildAt(i); 608 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 609 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem; 610 if (mDebug) { 611 Log.d(TAG, "testing item " + i); 612 } 613 if (!lp.isItemRemoved()) { 614 RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view); 615 assertSame("item position in LP should match adapter value :" + vh, 616 testAdapter.mItems.get(vh.mPosition), item); 617 } 618 } 619 } 620 621 RecyclerView.LayoutParams getLp(View v) { 622 return (RecyclerView.LayoutParams) v.getLayoutParams(); 623 } 624 625 protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) { 626 assertScrap(recycler); 627 if (mDebug) { 628 Log.d(TAG, "will layout items from " + start + " to " + end); 629 } 630 int diff = end > start ? 1 : -1; 631 int top = 0; 632 for (int i = start; i != end; i+=diff) { 633 if (mDebug) { 634 Log.d(TAG, "laying out item " + i); 635 } 636 View view = recycler.getViewForPosition(i); 637 assertNotNull("view should not be null for valid position. " 638 + "got null view at position " + i, view); 639 if (!mRecyclerView.mState.isPreLayout()) { 640 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view 641 .getLayoutParams(); 642 assertFalse("In post layout, getViewForPosition should never return a view " 643 + "that is removed", layoutParams != null 644 && layoutParams.isItemRemoved()); 645 646 } 647 assertEquals("getViewForPosition should return correct position", 648 i, getPosition(view)); 649 addView(view); 650 measureChildWithMargins(view, 0, 0); 651 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { 652 layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top, 653 getWidth(), top + getDecoratedMeasuredHeight(view)); 654 } else { 655 layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view) 656 , top + getDecoratedMeasuredHeight(view)); 657 } 658 659 top += view.getMeasuredHeight(); 660 } 661 } 662 663 private void assertScrap(RecyclerView.Recycler recycler) { 664 if (mRecyclerView.getAdapter() != null && 665 !mRecyclerView.getAdapter().hasStableIds()) { 666 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) { 667 assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid()); 668 } 669 } 670 } 671 672 @Override 673 public boolean canScrollHorizontally() { 674 return true; 675 } 676 677 @Override 678 public boolean canScrollVertically() { 679 return true; 680 } 681 682 @Override 683 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 684 RecyclerView.State state) { 685 mScrollHorizontallyAmount += dx; 686 return dx; 687 } 688 689 @Override 690 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 691 RecyclerView.State state) { 692 mScrollVerticallyAmount += dy; 693 return dy; 694 } 695 696 // START MOCKITO OVERRIDES 697 // We override package protected methods to make them public. This is necessary to run 698 // mockito on Kitkat 699 @Override 700 public void setRecyclerView(RecyclerView recyclerView) { 701 super.setRecyclerView(recyclerView); 702 } 703 704 @Override 705 public void dispatchAttachedToWindow(RecyclerView view) { 706 super.dispatchAttachedToWindow(view); 707 } 708 709 @Override 710 public void dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 711 super.dispatchDetachedFromWindow(view, recycler); 712 } 713 714 @Override 715 public void setExactMeasureSpecsFrom(RecyclerView recyclerView) { 716 super.setExactMeasureSpecsFrom(recyclerView); 717 } 718 719 @Override 720 public void setMeasureSpecs(int wSpec, int hSpec) { 721 super.setMeasureSpecs(wSpec, hSpec); 722 } 723 724 @Override 725 public void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { 726 super.setMeasuredDimensionFromChildren(widthSpec, heightSpec); 727 } 728 729 @Override 730 public boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, 731 RecyclerView.LayoutParams lp) { 732 return super.shouldReMeasureChild(child, widthSpec, heightSpec, lp); 733 } 734 735 @Override 736 public boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, 737 RecyclerView.LayoutParams lp) { 738 return super.shouldMeasureChild(child, widthSpec, heightSpec, lp); 739 } 740 741 @Override 742 public void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) { 743 super.removeAndRecycleScrapInt(recycler); 744 } 745 746 @Override 747 public void stopSmoothScroller() { 748 super.stopSmoothScroller(); 749 } 750 751 // END MOCKITO OVERRIDES 752 } 753 754 static class Item { 755 final static AtomicInteger idCounter = new AtomicInteger(0); 756 final public int mId = idCounter.incrementAndGet(); 757 758 int mAdapterIndex; 759 760 final String mText; 761 int mType = 0; 762 763 Item(int adapterIndex, String text) { 764 mAdapterIndex = adapterIndex; 765 mText = text; 766 } 767 768 @Override 769 public String toString() { 770 return "Item{" + 771 "mId=" + mId + 772 ", originalIndex=" + mAdapterIndex + 773 ", text='" + mText + '\'' + 774 '}'; 775 } 776 } 777 778 public class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 779 implements AttachDetachCountingAdapter { 780 781 public static final String DEFAULT_ITEM_PREFIX = "Item "; 782 783 ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter(); 784 List<Item> mItems; 785 786 public TestAdapter(int count) { 787 mItems = new ArrayList<Item>(count); 788 addItems(0, count, DEFAULT_ITEM_PREFIX); 789 } 790 791 private void addItems(int pos, int count, String prefix) { 792 for (int i = 0; i < count; i++, pos++) { 793 mItems.add(pos, new Item(pos, prefix)); 794 } 795 } 796 797 @Override 798 public int getItemViewType(int position) { 799 return getItemAt(position).mType; 800 } 801 802 @Override 803 public void onViewAttachedToWindow(TestViewHolder holder) { 804 super.onViewAttachedToWindow(holder); 805 mAttachmentCounter.onViewAttached(holder); 806 } 807 808 @Override 809 public void onViewDetachedFromWindow(TestViewHolder holder) { 810 super.onViewDetachedFromWindow(holder); 811 mAttachmentCounter.onViewDetached(holder); 812 } 813 814 @Override 815 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 816 super.onAttachedToRecyclerView(recyclerView); 817 mAttachmentCounter.onAttached(recyclerView); 818 } 819 820 @Override 821 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 822 super.onDetachedFromRecyclerView(recyclerView); 823 mAttachmentCounter.onDetached(recyclerView); 824 } 825 826 @Override 827 public TestViewHolder onCreateViewHolder(ViewGroup parent, 828 int viewType) { 829 TextView itemView = new TextView(parent.getContext()); 830 itemView.setFocusableInTouchMode(true); 831 itemView.setFocusable(true); 832 return new TestViewHolder(itemView); 833 } 834 835 @Override 836 public void onBindViewHolder(TestViewHolder holder, int position) { 837 assertNotNull(holder.mOwnerRecyclerView); 838 assertEquals(position, holder.getAdapterPosition()); 839 final Item item = mItems.get(position); 840 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")"); 841 holder.mBoundItem = item; 842 } 843 844 public Item getItemAt(int position) { 845 return mItems.get(position); 846 } 847 848 @Override 849 public void onViewRecycled(TestViewHolder holder) { 850 super.onViewRecycled(holder); 851 final int adapterPosition = holder.getAdapterPosition(); 852 final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && 853 !holder.isAdapterPositionUnknown() && !holder.isInvalid(); 854 String log = "Position check for " + holder.toString(); 855 assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION); 856 if (shouldHavePosition) { 857 assertTrue(log, mItems.size() > adapterPosition); 858 assertSame(log, holder.mBoundItem, mItems.get(adapterPosition)); 859 } 860 } 861 862 public void deleteAndNotify(final int start, final int count) throws Throwable { 863 deleteAndNotify(new int[]{start, count}); 864 } 865 866 /** 867 * Deletes items in the given ranges. 868 * <p> 869 * Note that each operation affects the one after so you should offset them properly. 870 * <p> 871 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 872 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 873 * A D E. Then it will delete 2,1 which means it will delete E. 874 */ 875 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 876 for (int[] tuple : startCountTuples) { 877 tuple[1] = -tuple[1]; 878 } 879 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 880 } 881 882 @Override 883 public long getItemId(int position) { 884 return hasStableIds() ? mItems.get(position).mId : super.getItemId(position); 885 } 886 887 public void offsetOriginalIndices(int start, int offset) { 888 for (int i = start; i < mItems.size(); i++) { 889 mItems.get(i).mAdapterIndex += offset; 890 } 891 } 892 893 /** 894 * @param start inclusive 895 * @param end exclusive 896 * @param offset 897 */ 898 public void offsetOriginalIndicesBetween(int start, int end, int offset) { 899 for (int i = start; i < end && i < mItems.size(); i++) { 900 mItems.get(i).mAdapterIndex += offset; 901 } 902 } 903 904 public void addAndNotify(final int count) throws Throwable { 905 assertEquals(0, mItems.size()); 906 new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}).runOnMainThread(); 907 } 908 909 public void resetItemsTo(final List<Item> testItems) throws Throwable { 910 if (!mItems.isEmpty()) { 911 deleteAndNotify(0, mItems.size()); 912 } 913 mItems = testItems; 914 runTestOnUiThread(new Runnable() { 915 @Override 916 public void run() { 917 notifyItemRangeInserted(0, testItems.size()); 918 } 919 }); 920 } 921 922 public void addAndNotify(final int start, final int count) throws Throwable { 923 addAndNotify(new int[]{start, count}); 924 } 925 926 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 927 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 928 } 929 930 public void dispatchDataSetChanged() throws Throwable { 931 runTestOnUiThread(new Runnable() { 932 @Override 933 public void run() { 934 notifyDataSetChanged(); 935 } 936 }); 937 } 938 939 public void changeAndNotify(final int start, final int count) throws Throwable { 940 runTestOnUiThread(new Runnable() { 941 @Override 942 public void run() { 943 notifyItemRangeChanged(start, count); 944 } 945 }); 946 } 947 948 public void changeAndNotifyWithPayload(final int start, final int count, 949 final Object payload) throws Throwable { 950 runTestOnUiThread(new Runnable() { 951 @Override 952 public void run() { 953 notifyItemRangeChanged(start, count, payload); 954 } 955 }); 956 } 957 958 public void changePositionsAndNotify(final int... positions) throws Throwable { 959 runTestOnUiThread(new Runnable() { 960 @Override 961 public void run() { 962 for (int i = 0; i < positions.length; i += 1) { 963 TestAdapter.super.notifyItemRangeChanged(positions[i], 1); 964 } 965 } 966 }); 967 } 968 969 /** 970 * Similar to other methods but negative count means delete and position count means add. 971 * <p> 972 * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an 973 * item to index 1, then remove an item from index 2 (updated index 2) 974 */ 975 public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable { 976 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 977 } 978 979 @Override 980 public int getItemCount() { 981 return mItems.size(); 982 } 983 984 /** 985 * Uses notifyDataSetChanged 986 */ 987 public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable { 988 for (int i = 0; i < fromToTuples.length; i += 1) { 989 int[] tuple = fromToTuples[i]; 990 moveItem(tuple[0], tuple[1], false); 991 } 992 if (notifyChange) { 993 dispatchDataSetChanged(); 994 } 995 } 996 997 /** 998 * Uses notifyDataSetChanged 999 */ 1000 public void moveItem(final int from, final int to, final boolean notifyChange) 1001 throws Throwable { 1002 runTestOnUiThread(new Runnable() { 1003 @Override 1004 public void run() { 1005 moveInUIThread(from, to); 1006 if (notifyChange) { 1007 notifyDataSetChanged(); 1008 } 1009 } 1010 }); 1011 } 1012 1013 /** 1014 * Uses notifyItemMoved 1015 */ 1016 public void moveAndNotify(final int from, final int to) throws Throwable { 1017 runTestOnUiThread(new Runnable() { 1018 @Override 1019 public void run() { 1020 moveInUIThread(from, to); 1021 notifyItemMoved(from, to); 1022 } 1023 }); 1024 } 1025 1026 public void clearOnUIThread() { 1027 assertEquals("clearOnUIThread called from a wrong thread", 1028 Looper.getMainLooper(), Looper.myLooper()); 1029 mItems = new ArrayList<Item>(); 1030 notifyDataSetChanged(); 1031 } 1032 1033 protected void moveInUIThread(int from, int to) { 1034 Item item = mItems.remove(from); 1035 offsetOriginalIndices(from, -1); 1036 mItems.add(to, item); 1037 offsetOriginalIndices(to + 1, 1); 1038 item.mAdapterIndex = to; 1039 } 1040 1041 1042 @Override 1043 public ViewAttachDetachCounter getCounter() { 1044 return mAttachmentCounter; 1045 } 1046 1047 private class AddRemoveRunnable implements Runnable { 1048 final String mNewItemPrefix; 1049 final int[][] mStartCountTuples; 1050 1051 public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) { 1052 mNewItemPrefix = newItemPrefix; 1053 mStartCountTuples = startCountTuples; 1054 } 1055 1056 public AddRemoveRunnable(int[][] startCountTuples) { 1057 this("new item ", startCountTuples); 1058 } 1059 1060 public void runOnMainThread() throws Throwable { 1061 if (Looper.myLooper() == Looper.getMainLooper()) { 1062 run(); 1063 } else { 1064 runTestOnUiThread(this); 1065 } 1066 } 1067 1068 @Override 1069 public void run() { 1070 for (int[] tuple : mStartCountTuples) { 1071 if (tuple[1] < 0) { 1072 delete(tuple); 1073 } else { 1074 add(tuple); 1075 } 1076 } 1077 } 1078 1079 private void add(int[] tuple) { 1080 // offset others 1081 offsetOriginalIndices(tuple[0], tuple[1]); 1082 addItems(tuple[0], tuple[1], mNewItemPrefix); 1083 notifyItemRangeInserted(tuple[0], tuple[1]); 1084 } 1085 1086 private void delete(int[] tuple) { 1087 final int count = -tuple[1]; 1088 offsetOriginalIndices(tuple[0] + count, tuple[1]); 1089 for (int i = 0; i < count; i++) { 1090 mItems.remove(tuple[0]); 1091 } 1092 notifyItemRangeRemoved(tuple[0], count); 1093 } 1094 } 1095 } 1096 1097 public boolean isMainThread() { 1098 return Looper.myLooper() == Looper.getMainLooper(); 1099 } 1100 1101 public void runTestOnUiThread(Runnable r) throws Throwable { 1102 if (Looper.myLooper() == Looper.getMainLooper()) { 1103 r.run(); 1104 } else { 1105 InstrumentationRegistry.getInstrumentation().runOnMainSync(r); 1106 } 1107 } 1108 1109 static class TargetTuple { 1110 1111 final int mPosition; 1112 1113 final int mLayoutDirection; 1114 1115 TargetTuple(int position, int layoutDirection) { 1116 this.mPosition = position; 1117 this.mLayoutDirection = layoutDirection; 1118 } 1119 1120 @Override 1121 public String toString() { 1122 return "TargetTuple{" + 1123 "mPosition=" + mPosition + 1124 ", mLayoutDirection=" + mLayoutDirection + 1125 '}'; 1126 } 1127 } 1128 1129 public interface AttachDetachCountingAdapter { 1130 1131 ViewAttachDetachCounter getCounter(); 1132 } 1133 1134 public class ViewAttachDetachCounter { 1135 1136 Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>(); 1137 1138 public void validateRemaining(RecyclerView recyclerView) { 1139 final int childCount = recyclerView.getChildCount(); 1140 for (int i = 0; i < childCount; i++) { 1141 View view = recyclerView.getChildAt(i); 1142 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 1143 assertTrue("remaining view should be in attached set " + vh, 1144 mAttachedSet.contains(vh)); 1145 } 1146 assertEquals("there should not be any views left in attached set", 1147 childCount, mAttachedSet.size()); 1148 } 1149 1150 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 1151 try { 1152 assertTrue("view holder should be in attached set", 1153 mAttachedSet.remove(viewHolder)); 1154 } catch (Throwable t) { 1155 postExceptionToInstrumentation(t); 1156 } 1157 } 1158 1159 public void onViewAttached(RecyclerView.ViewHolder viewHolder) { 1160 try { 1161 assertTrue("view holder should not be in attached set", 1162 mAttachedSet.add(viewHolder)); 1163 } catch (Throwable t) { 1164 postExceptionToInstrumentation(t); 1165 } 1166 } 1167 1168 public void onAttached(RecyclerView recyclerView) { 1169 // when a new RV is attached, clear the set and add all view holders 1170 mAttachedSet.clear(); 1171 final int childCount = recyclerView.getChildCount(); 1172 for (int i = 0; i < childCount; i ++) { 1173 View view = recyclerView.getChildAt(i); 1174 mAttachedSet.add(recyclerView.getChildViewHolder(view)); 1175 } 1176 } 1177 1178 public void onDetached(RecyclerView recyclerView) { 1179 validateRemaining(recyclerView); 1180 } 1181 } 1182} 1183