BaseRecyclerViewInstrumentationTest.java revision 178c4e15a0ebb501d2bb4f3ac3176e4c44954954
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 Object mData; 531 532 public TestViewHolder(View itemView) { 533 super(itemView); 534 itemView.setFocusable(true); 535 } 536 537 @Override 538 public String toString() { 539 return super.toString() + " item:" + mBoundItem + ", data:" + mData; 540 } 541 542 public Object getData() { 543 return mData; 544 } 545 546 public void setData(Object data) { 547 mData = data; 548 } 549 } 550 class DumbLayoutManager extends TestLayoutManager { 551 @Override 552 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 553 detachAndScrapAttachedViews(recycler); 554 layoutRange(recycler, 0, state.getItemCount()); 555 if (layoutLatch != null) { 556 layoutLatch.countDown(); 557 } 558 } 559 } 560 public class TestLayoutManager extends RecyclerView.LayoutManager { 561 int mScrollVerticallyAmount; 562 int mScrollHorizontallyAmount; 563 protected CountDownLatch layoutLatch; 564 private boolean mSupportsPredictive = false; 565 566 public void expectLayouts(int count) { 567 layoutLatch = new CountDownLatch(count); 568 } 569 570 public void waitForLayout(int seconds) throws Throwable { 571 layoutLatch.await(seconds * (mDebug ? 1000 : 1), SECONDS); 572 checkForMainThreadException(); 573 MatcherAssert.assertThat("all layouts should complete on time", 574 layoutLatch.getCount(), CoreMatchers.is(0L)); 575 // use a runnable to ensure RV layout is finished 576 getInstrumentation().runOnMainSync(new Runnable() { 577 @Override 578 public void run() { 579 } 580 }); 581 } 582 583 public boolean isSupportsPredictive() { 584 return mSupportsPredictive; 585 } 586 587 public void setSupportsPredictive(boolean supportsPredictive) { 588 mSupportsPredictive = supportsPredictive; 589 } 590 591 @Override 592 public boolean supportsPredictiveItemAnimations() { 593 return mSupportsPredictive; 594 } 595 596 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 597 layoutLatch.await(timeout, TimeUnit.SECONDS); 598 assertEquals(msg, count, layoutLatch.getCount()); 599 } 600 601 public void assertNoLayout(String msg, long timeout) throws Throwable { 602 layoutLatch.await(timeout, TimeUnit.SECONDS); 603 assertFalse(msg, layoutLatch.getCount() == 0); 604 } 605 606 @Override 607 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 608 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 609 ViewGroup.LayoutParams.WRAP_CONTENT); 610 } 611 612 void assertVisibleItemPositions() { 613 int i = getChildCount(); 614 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 615 while (i-- > 0) { 616 View view = getChildAt(i); 617 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 618 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem; 619 if (mDebug) { 620 Log.d(TAG, "testing item " + i); 621 } 622 if (!lp.isItemRemoved()) { 623 RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view); 624 assertSame("item position in LP should match adapter value :" + vh, 625 testAdapter.mItems.get(vh.mPosition), item); 626 } 627 } 628 } 629 630 RecyclerView.LayoutParams getLp(View v) { 631 return (RecyclerView.LayoutParams) v.getLayoutParams(); 632 } 633 634 protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) { 635 assertScrap(recycler); 636 if (mDebug) { 637 Log.d(TAG, "will layout items from " + start + " to " + end); 638 } 639 int diff = end > start ? 1 : -1; 640 int top = 0; 641 for (int i = start; i != end; i+=diff) { 642 if (mDebug) { 643 Log.d(TAG, "laying out item " + i); 644 } 645 View view = recycler.getViewForPosition(i); 646 assertNotNull("view should not be null for valid position. " 647 + "got null view at position " + i, view); 648 if (!mRecyclerView.mState.isPreLayout()) { 649 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view 650 .getLayoutParams(); 651 assertFalse("In post layout, getViewForPosition should never return a view " 652 + "that is removed", layoutParams != null 653 && layoutParams.isItemRemoved()); 654 655 } 656 assertEquals("getViewForPosition should return correct position", 657 i, getPosition(view)); 658 addView(view); 659 measureChildWithMargins(view, 0, 0); 660 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { 661 layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top, 662 getWidth(), top + getDecoratedMeasuredHeight(view)); 663 } else { 664 layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view) 665 , top + getDecoratedMeasuredHeight(view)); 666 } 667 668 top += view.getMeasuredHeight(); 669 } 670 } 671 672 private void assertScrap(RecyclerView.Recycler recycler) { 673 if (mRecyclerView.getAdapter() != null && 674 !mRecyclerView.getAdapter().hasStableIds()) { 675 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) { 676 assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid()); 677 } 678 } 679 } 680 681 @Override 682 public boolean canScrollHorizontally() { 683 return true; 684 } 685 686 @Override 687 public boolean canScrollVertically() { 688 return true; 689 } 690 691 @Override 692 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 693 RecyclerView.State state) { 694 mScrollHorizontallyAmount += dx; 695 return dx; 696 } 697 698 @Override 699 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 700 RecyclerView.State state) { 701 mScrollVerticallyAmount += dy; 702 return dy; 703 } 704 705 // START MOCKITO OVERRIDES 706 // We override package protected methods to make them public. This is necessary to run 707 // mockito on Kitkat 708 @Override 709 public void setRecyclerView(RecyclerView recyclerView) { 710 super.setRecyclerView(recyclerView); 711 } 712 713 @Override 714 public void dispatchAttachedToWindow(RecyclerView view) { 715 super.dispatchAttachedToWindow(view); 716 } 717 718 @Override 719 public void dispatchDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { 720 super.dispatchDetachedFromWindow(view, recycler); 721 } 722 723 @Override 724 public void setExactMeasureSpecsFrom(RecyclerView recyclerView) { 725 super.setExactMeasureSpecsFrom(recyclerView); 726 } 727 728 @Override 729 public void setMeasureSpecs(int wSpec, int hSpec) { 730 super.setMeasureSpecs(wSpec, hSpec); 731 } 732 733 @Override 734 public void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { 735 super.setMeasuredDimensionFromChildren(widthSpec, heightSpec); 736 } 737 738 @Override 739 public boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, 740 RecyclerView.LayoutParams lp) { 741 return super.shouldReMeasureChild(child, widthSpec, heightSpec, lp); 742 } 743 744 @Override 745 public boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, 746 RecyclerView.LayoutParams lp) { 747 return super.shouldMeasureChild(child, widthSpec, heightSpec, lp); 748 } 749 750 @Override 751 public void removeAndRecycleScrapInt(RecyclerView.Recycler recycler) { 752 super.removeAndRecycleScrapInt(recycler); 753 } 754 755 @Override 756 public void stopSmoothScroller() { 757 super.stopSmoothScroller(); 758 } 759 760 // END MOCKITO OVERRIDES 761 } 762 763 static class Item { 764 final static AtomicInteger idCounter = new AtomicInteger(0); 765 final public int mId = idCounter.incrementAndGet(); 766 767 int mAdapterIndex; 768 769 final String mText; 770 int mType = 0; 771 772 Item(int adapterIndex, String text) { 773 mAdapterIndex = adapterIndex; 774 mText = text; 775 } 776 777 @Override 778 public String toString() { 779 return "Item{" + 780 "mId=" + mId + 781 ", originalIndex=" + mAdapterIndex + 782 ", text='" + mText + '\'' + 783 '}'; 784 } 785 } 786 787 public class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 788 implements AttachDetachCountingAdapter { 789 790 public static final String DEFAULT_ITEM_PREFIX = "Item "; 791 792 ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter(); 793 List<Item> mItems; 794 795 public TestAdapter(int count) { 796 mItems = new ArrayList<Item>(count); 797 addItems(0, count, DEFAULT_ITEM_PREFIX); 798 } 799 800 private void addItems(int pos, int count, String prefix) { 801 for (int i = 0; i < count; i++, pos++) { 802 mItems.add(pos, new Item(pos, prefix)); 803 } 804 } 805 806 @Override 807 public int getItemViewType(int position) { 808 return getItemAt(position).mType; 809 } 810 811 @Override 812 public void onViewAttachedToWindow(TestViewHolder holder) { 813 super.onViewAttachedToWindow(holder); 814 mAttachmentCounter.onViewAttached(holder); 815 } 816 817 @Override 818 public void onViewDetachedFromWindow(TestViewHolder holder) { 819 super.onViewDetachedFromWindow(holder); 820 mAttachmentCounter.onViewDetached(holder); 821 } 822 823 @Override 824 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 825 super.onAttachedToRecyclerView(recyclerView); 826 mAttachmentCounter.onAttached(recyclerView); 827 } 828 829 @Override 830 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 831 super.onDetachedFromRecyclerView(recyclerView); 832 mAttachmentCounter.onDetached(recyclerView); 833 } 834 835 @Override 836 public TestViewHolder onCreateViewHolder(ViewGroup parent, 837 int viewType) { 838 TextView itemView = new TextView(parent.getContext()); 839 itemView.setFocusableInTouchMode(true); 840 itemView.setFocusable(true); 841 return new TestViewHolder(itemView); 842 } 843 844 @Override 845 public void onBindViewHolder(TestViewHolder holder, int position) { 846 assertNotNull(holder.mOwnerRecyclerView); 847 assertEquals(position, holder.getAdapterPosition()); 848 final Item item = mItems.get(position); 849 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")"); 850 holder.mBoundItem = item; 851 } 852 853 public Item getItemAt(int position) { 854 return mItems.get(position); 855 } 856 857 @Override 858 public void onViewRecycled(TestViewHolder holder) { 859 super.onViewRecycled(holder); 860 final int adapterPosition = holder.getAdapterPosition(); 861 final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && 862 !holder.isAdapterPositionUnknown() && !holder.isInvalid(); 863 String log = "Position check for " + holder.toString(); 864 assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION); 865 if (shouldHavePosition) { 866 assertTrue(log, mItems.size() > adapterPosition); 867 assertSame(log, holder.mBoundItem, mItems.get(adapterPosition)); 868 } 869 } 870 871 public void deleteAndNotify(final int start, final int count) throws Throwable { 872 deleteAndNotify(new int[]{start, count}); 873 } 874 875 /** 876 * Deletes items in the given ranges. 877 * <p> 878 * Note that each operation affects the one after so you should offset them properly. 879 * <p> 880 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 881 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 882 * A D E. Then it will delete 2,1 which means it will delete E. 883 */ 884 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 885 for (int[] tuple : startCountTuples) { 886 tuple[1] = -tuple[1]; 887 } 888 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 889 } 890 891 @Override 892 public long getItemId(int position) { 893 return hasStableIds() ? mItems.get(position).mId : super.getItemId(position); 894 } 895 896 public void offsetOriginalIndices(int start, int offset) { 897 for (int i = start; i < mItems.size(); i++) { 898 mItems.get(i).mAdapterIndex += offset; 899 } 900 } 901 902 /** 903 * @param start inclusive 904 * @param end exclusive 905 * @param offset 906 */ 907 public void offsetOriginalIndicesBetween(int start, int end, int offset) { 908 for (int i = start; i < end && i < mItems.size(); i++) { 909 mItems.get(i).mAdapterIndex += offset; 910 } 911 } 912 913 public void addAndNotify(final int count) throws Throwable { 914 assertEquals(0, mItems.size()); 915 new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}).runOnMainThread(); 916 } 917 918 public void resetItemsTo(final List<Item> testItems) throws Throwable { 919 if (!mItems.isEmpty()) { 920 deleteAndNotify(0, mItems.size()); 921 } 922 mItems = testItems; 923 runTestOnUiThread(new Runnable() { 924 @Override 925 public void run() { 926 notifyItemRangeInserted(0, testItems.size()); 927 } 928 }); 929 } 930 931 public void addAndNotify(final int start, final int count) throws Throwable { 932 addAndNotify(new int[]{start, count}); 933 } 934 935 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 936 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 937 } 938 939 public void dispatchDataSetChanged() throws Throwable { 940 runTestOnUiThread(new Runnable() { 941 @Override 942 public void run() { 943 notifyDataSetChanged(); 944 } 945 }); 946 } 947 948 public void changeAndNotify(final int start, final int count) throws Throwable { 949 runTestOnUiThread(new Runnable() { 950 @Override 951 public void run() { 952 notifyItemRangeChanged(start, count); 953 } 954 }); 955 } 956 957 public void changeAndNotifyWithPayload(final int start, final int count, 958 final Object payload) throws Throwable { 959 runTestOnUiThread(new Runnable() { 960 @Override 961 public void run() { 962 notifyItemRangeChanged(start, count, payload); 963 } 964 }); 965 } 966 967 public void changePositionsAndNotify(final int... positions) throws Throwable { 968 runTestOnUiThread(new Runnable() { 969 @Override 970 public void run() { 971 for (int i = 0; i < positions.length; i += 1) { 972 TestAdapter.super.notifyItemRangeChanged(positions[i], 1); 973 } 974 } 975 }); 976 } 977 978 /** 979 * Similar to other methods but negative count means delete and position count means add. 980 * <p> 981 * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an 982 * item to index 1, then remove an item from index 2 (updated index 2) 983 */ 984 public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable { 985 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 986 } 987 988 @Override 989 public int getItemCount() { 990 return mItems.size(); 991 } 992 993 /** 994 * Uses notifyDataSetChanged 995 */ 996 public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable { 997 for (int i = 0; i < fromToTuples.length; i += 1) { 998 int[] tuple = fromToTuples[i]; 999 moveItem(tuple[0], tuple[1], false); 1000 } 1001 if (notifyChange) { 1002 dispatchDataSetChanged(); 1003 } 1004 } 1005 1006 /** 1007 * Uses notifyDataSetChanged 1008 */ 1009 public void moveItem(final int from, final int to, final boolean notifyChange) 1010 throws Throwable { 1011 runTestOnUiThread(new Runnable() { 1012 @Override 1013 public void run() { 1014 moveInUIThread(from, to); 1015 if (notifyChange) { 1016 notifyDataSetChanged(); 1017 } 1018 } 1019 }); 1020 } 1021 1022 /** 1023 * Uses notifyItemMoved 1024 */ 1025 public void moveAndNotify(final int from, final int to) throws Throwable { 1026 runTestOnUiThread(new Runnable() { 1027 @Override 1028 public void run() { 1029 moveInUIThread(from, to); 1030 notifyItemMoved(from, to); 1031 } 1032 }); 1033 } 1034 1035 public void clearOnUIThread() { 1036 assertEquals("clearOnUIThread called from a wrong thread", 1037 Looper.getMainLooper(), Looper.myLooper()); 1038 mItems = new ArrayList<Item>(); 1039 notifyDataSetChanged(); 1040 } 1041 1042 protected void moveInUIThread(int from, int to) { 1043 Item item = mItems.remove(from); 1044 offsetOriginalIndices(from, -1); 1045 mItems.add(to, item); 1046 offsetOriginalIndices(to + 1, 1); 1047 item.mAdapterIndex = to; 1048 } 1049 1050 1051 @Override 1052 public ViewAttachDetachCounter getCounter() { 1053 return mAttachmentCounter; 1054 } 1055 1056 private class AddRemoveRunnable implements Runnable { 1057 final String mNewItemPrefix; 1058 final int[][] mStartCountTuples; 1059 1060 public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) { 1061 mNewItemPrefix = newItemPrefix; 1062 mStartCountTuples = startCountTuples; 1063 } 1064 1065 public AddRemoveRunnable(int[][] startCountTuples) { 1066 this("new item ", startCountTuples); 1067 } 1068 1069 public void runOnMainThread() throws Throwable { 1070 if (Looper.myLooper() == Looper.getMainLooper()) { 1071 run(); 1072 } else { 1073 runTestOnUiThread(this); 1074 } 1075 } 1076 1077 @Override 1078 public void run() { 1079 for (int[] tuple : mStartCountTuples) { 1080 if (tuple[1] < 0) { 1081 delete(tuple); 1082 } else { 1083 add(tuple); 1084 } 1085 } 1086 } 1087 1088 private void add(int[] tuple) { 1089 // offset others 1090 offsetOriginalIndices(tuple[0], tuple[1]); 1091 addItems(tuple[0], tuple[1], mNewItemPrefix); 1092 notifyItemRangeInserted(tuple[0], tuple[1]); 1093 } 1094 1095 private void delete(int[] tuple) { 1096 final int count = -tuple[1]; 1097 offsetOriginalIndices(tuple[0] + count, tuple[1]); 1098 for (int i = 0; i < count; i++) { 1099 mItems.remove(tuple[0]); 1100 } 1101 notifyItemRangeRemoved(tuple[0], count); 1102 } 1103 } 1104 } 1105 1106 public boolean isMainThread() { 1107 return Looper.myLooper() == Looper.getMainLooper(); 1108 } 1109 1110 public void runTestOnUiThread(Runnable r) throws Throwable { 1111 if (Looper.myLooper() == Looper.getMainLooper()) { 1112 r.run(); 1113 } else { 1114 InstrumentationRegistry.getInstrumentation().runOnMainSync(r); 1115 } 1116 } 1117 1118 static class TargetTuple { 1119 1120 final int mPosition; 1121 1122 final int mLayoutDirection; 1123 1124 TargetTuple(int position, int layoutDirection) { 1125 this.mPosition = position; 1126 this.mLayoutDirection = layoutDirection; 1127 } 1128 1129 @Override 1130 public String toString() { 1131 return "TargetTuple{" + 1132 "mPosition=" + mPosition + 1133 ", mLayoutDirection=" + mLayoutDirection + 1134 '}'; 1135 } 1136 } 1137 1138 public interface AttachDetachCountingAdapter { 1139 1140 ViewAttachDetachCounter getCounter(); 1141 } 1142 1143 public class ViewAttachDetachCounter { 1144 1145 Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>(); 1146 1147 public void validateRemaining(RecyclerView recyclerView) { 1148 final int childCount = recyclerView.getChildCount(); 1149 for (int i = 0; i < childCount; i++) { 1150 View view = recyclerView.getChildAt(i); 1151 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 1152 assertTrue("remaining view should be in attached set " + vh, 1153 mAttachedSet.contains(vh)); 1154 } 1155 assertEquals("there should not be any views left in attached set", 1156 childCount, mAttachedSet.size()); 1157 } 1158 1159 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 1160 try { 1161 assertTrue("view holder should be in attached set", 1162 mAttachedSet.remove(viewHolder)); 1163 } catch (Throwable t) { 1164 postExceptionToInstrumentation(t); 1165 } 1166 } 1167 1168 public void onViewAttached(RecyclerView.ViewHolder viewHolder) { 1169 try { 1170 assertTrue("view holder should not be in attached set", 1171 mAttachedSet.add(viewHolder)); 1172 } catch (Throwable t) { 1173 postExceptionToInstrumentation(t); 1174 } 1175 } 1176 1177 public void onAttached(RecyclerView recyclerView) { 1178 // when a new RV is attached, clear the set and add all view holders 1179 mAttachedSet.clear(); 1180 final int childCount = recyclerView.getChildCount(); 1181 for (int i = 0; i < childCount; i ++) { 1182 View view = recyclerView.getChildAt(i); 1183 mAttachedSet.add(recyclerView.getChildViewHolder(view)); 1184 } 1185 } 1186 1187 public void onDetached(RecyclerView recyclerView) { 1188 validateRemaining(recyclerView); 1189 } 1190 } 1191} 1192