BaseRecyclerViewInstrumentationTest.java revision 11b7cfea15306c0d6e4ed7da09f7698df01695df
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 android.test.ActivityInstrumentationTestCase2; 20import android.util.Log; 21import android.view.View; 22import android.view.ViewGroup; 23import android.widget.TextView; 24 25import java.util.ArrayList; 26import java.util.List; 27import java.util.concurrent.CountDownLatch; 28import java.util.concurrent.TimeUnit; 29import java.util.concurrent.atomic.AtomicInteger; 30 31abstract public class BaseRecyclerViewInstrumentationTest extends 32 ActivityInstrumentationTestCase2<TestActivity> { 33 34 private static final String TAG = "RecyclerViewTest"; 35 36 private boolean mDebug; 37 38 protected RecyclerView mRecyclerView; 39 40 public BaseRecyclerViewInstrumentationTest() { 41 this(false); 42 } 43 44 public BaseRecyclerViewInstrumentationTest(boolean debug) { 45 super("android.support.v7.widget", TestActivity.class); 46 mDebug = debug; 47 } 48 49 public void removeRecyclerView() throws Throwable { 50 mRecyclerView = null; 51 runTestOnUiThread(new Runnable() { 52 @Override 53 public void run() { 54 getActivity().mContainer.removeAllViews(); 55 } 56 }); 57 } 58 59 public void setRecyclerView(final RecyclerView recyclerView) throws Throwable { 60 mRecyclerView = recyclerView; 61 runTestOnUiThread(new Runnable() { 62 @Override 63 public void run() { 64 getActivity().mContainer.addView(recyclerView); 65 } 66 }); 67 } 68 69 public void requestLayoutOnUIThread(final View view) throws Throwable { 70 runTestOnUiThread(new Runnable() { 71 @Override 72 public void run() { 73 view.requestLayout(); 74 } 75 }); 76 } 77 78 public void scrollBy(final int dt) throws Throwable { 79 runTestOnUiThread(new Runnable() { 80 @Override 81 public void run() { 82 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 83 mRecyclerView.scrollBy(dt, 0); 84 } else { 85 mRecyclerView.scrollBy(0, dt); 86 } 87 88 } 89 }); 90 } 91 92 void scrollToPosition(final int position) throws Throwable { 93 runTestOnUiThread(new Runnable() { 94 @Override 95 public void run() { 96 mRecyclerView.getLayoutManager().scrollToPosition(position); 97 } 98 }); 99 } 100 101 void smoothScrollToPosition(final int position) 102 throws Throwable { 103 Log.d(TAG, "SMOOTH scrolling to " + position); 104 runTestOnUiThread(new Runnable() { 105 @Override 106 public void run() { 107 mRecyclerView.smoothScrollToPosition(position); 108 } 109 }); 110 while (mRecyclerView.getLayoutManager().isSmoothScrolling() || 111 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 112 if (mDebug) { 113 Log.d(TAG, "SMOOTH scrolling step"); 114 } 115 Thread.sleep(200); 116 } 117 Log.d(TAG, "SMOOTH scrolling done"); 118 } 119 120 class TestViewHolder extends RecyclerView.ViewHolder { 121 122 Item mBindedItem; 123 124 public TestViewHolder(View itemView) { 125 super(itemView); 126 } 127 } 128 129 class TestLayoutManager extends RecyclerView.LayoutManager { 130 131 CountDownLatch layoutLatch; 132 133 public void expectLayouts(int count) { 134 layoutLatch = new CountDownLatch(count); 135 } 136 137 public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable { 138 layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit); 139 assertEquals("all expected layouts should be executed at the expected time", 140 0, layoutLatch.getCount()); 141 } 142 143 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 144 layoutLatch.await(timeout, TimeUnit.SECONDS); 145 assertEquals(msg, count, layoutLatch.getCount()); 146 } 147 148 public void assertNoLayout(String msg, long timeout) throws Throwable { 149 layoutLatch.await(timeout, TimeUnit.SECONDS); 150 assertFalse(msg, layoutLatch.getCount() == 0); 151 } 152 153 public void waitForLayout(long timeout) throws Throwable { 154 waitForLayout(timeout, TimeUnit.SECONDS); 155 } 156 157 @Override 158 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 159 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 160 ViewGroup.LayoutParams.WRAP_CONTENT); 161 } 162 163 void assertVisibleItemPositions() { 164 int i = getChildCount(); 165 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 166 while (i-- > 0) { 167 View view = getChildAt(i); 168 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 169 Item item = ((TestViewHolder) lp.mViewHolder).mBindedItem; 170 if (mDebug) { 171 Log.d(TAG, "testing item " + i); 172 } 173 assertSame("item position in LP should match adapter value", 174 testAdapter.mItems.get(lp.getViewPosition()), item); 175 } 176 } 177 178 RecyclerView.LayoutParams getLp(View v) { 179 return (RecyclerView.LayoutParams) v.getLayoutParams(); 180 } 181 182 void layoutRange(RecyclerView.Recycler recycler, int start, 183 int end) { 184 if (mDebug) { 185 Log.d(TAG, "will layout items from " + start + " to " + end); 186 } 187 for (int i = start; i < end; i++) { 188 if (mDebug) { 189 Log.d(TAG, "laying out item " + i); 190 } 191 View view = recycler.getViewForPosition(i); 192 assertNotNull("view should not be null for valid position. " 193 + "got null view at position " + i, view); 194 if (!getLp(view).isItemRemoved()) { 195 addView(view); 196 } 197 198 measureChildWithMargins(view, 0, 0); 199 layoutDecorated(view, 0, (i - start) * 10, getDecoratedMeasuredWidth(view) 200 , getDecoratedMeasuredHeight(view)); 201 } 202 } 203 } 204 205 static class Item { 206 final static AtomicInteger idCounter = new AtomicInteger(0); 207 final public int mId = idCounter.incrementAndGet(); 208 209 int originalIndex; 210 211 final String text; 212 213 Item(int originalIndex, String text) { 214 this.originalIndex = originalIndex; 215 this.text = text; 216 } 217 } 218 219 class TestAdapter extends RecyclerView.Adapter<TestViewHolder> { 220 221 List<Item> mItems; 222 223 TestAdapter(int count) { 224 mItems = new ArrayList<Item>(count); 225 for (int i = 0; i < count; i++) { 226 mItems.add(new Item(i, "Item " + i)); 227 } 228 } 229 230 @Override 231 public TestViewHolder onCreateViewHolder(ViewGroup parent, 232 int viewType) { 233 return new TestViewHolder(new TextView(parent.getContext())); 234 } 235 236 @Override 237 public void onBindViewHolder(TestViewHolder holder, int position) { 238 final Item item = mItems.get(position); 239 ((TextView) (holder.itemView)).setText(item.text); 240 holder.mBindedItem = item; 241 } 242 243 public void deleteAndNotify(final int start, final int count) throws Throwable { 244 deleteAndNotify(new int[]{start, count}); 245 } 246 247 /** 248 * Deletes items in the given ranges. 249 * <p> 250 * Note that each operation affects the one after so you should offset them properly. 251 * <p> 252 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 253 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 254 * A D E. Then it will delete 2,1 which means it will delete E. 255 */ 256 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 257 runTestOnUiThread(new Runnable() { 258 @Override 259 public void run() { 260 for (int t = 0; t < startCountTuples.length; t++) { 261 int[] tuple = startCountTuples[t]; 262 for (int i = 0; i < tuple[1]; i++) { 263 mItems.remove(tuple[0]); 264 } 265 notifyItemRangeRemoved(tuple[0], tuple[1]); 266 } 267 268 } 269 }); 270 } 271 272 public void addAndNotify(final int start, final int count) throws Throwable { 273 addAndNotify(new int[]{start, count}); 274 } 275 276 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 277 runTestOnUiThread(new Runnable() { 278 @Override 279 public void run() { 280 for (int t = 0; t < startCountTuples.length; t++) { 281 int[] tuple = startCountTuples[t]; 282 for (int i = 0; i < tuple[1]; i++) { 283 mItems.add(tuple[0], new Item(i, "new item " + i)); 284 } 285 // offset others 286 for (int i = tuple[0] + tuple[1]; i < mItems.size(); i++) { 287 mItems.get(i).originalIndex += tuple[1]; 288 } 289 notifyItemRangeInserted(tuple[0], tuple[1]); 290 } 291 292 } 293 }); 294 } 295 296 @Override 297 public int getItemCount() { 298 return mItems.size(); 299 } 300 } 301}