BaseRecyclerViewInstrumentationTest.java revision 7e9da1a2629b657d56e2f54abd18c6249f3be037
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.os.Looper;
20import android.test.ActivityInstrumentationTestCase2;
21import android.util.Log;
22import android.view.View;
23import android.view.ViewGroup;
24import android.widget.TextView;
25
26import java.util.ArrayList;
27import java.util.List;
28import java.util.concurrent.CountDownLatch;
29import java.util.concurrent.TimeUnit;
30import java.util.concurrent.atomic.AtomicInteger;
31
32abstract public class BaseRecyclerViewInstrumentationTest extends
33        ActivityInstrumentationTestCase2<TestActivity> {
34
35    private static final String TAG = "RecyclerViewTest";
36
37    private boolean mDebug;
38
39    protected RecyclerView mRecyclerView;
40
41    protected AdapterHelper mAdapterHelper;
42
43    Throwable mainThreadException;
44
45    public BaseRecyclerViewInstrumentationTest() {
46        this(false);
47    }
48
49    public BaseRecyclerViewInstrumentationTest(boolean debug) {
50        super("android.support.v7.recyclerview", TestActivity.class);
51        mDebug = debug;
52    }
53
54    void checkForMainThreadException() throws Throwable {
55        if (mainThreadException != null) {
56            throw mainThreadException;
57        }
58    }
59
60    void postExceptionToInstrumentation(Throwable t) {
61        if (mDebug) {
62            Log.e(TAG, "captured exception on main thread", t);
63        }
64        mainThreadException = t;
65        if (mRecyclerView != null && mRecyclerView
66                .getLayoutManager() instanceof TestLayoutManager) {
67            TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager();
68            // finish all layouts so that we get the correct exception
69            while (lm.layoutLatch.getCount() > 0) {
70                lm.layoutLatch.countDown();
71            }
72        }
73    }
74
75    @Override
76    protected void tearDown() throws Exception {
77        if (mRecyclerView != null) {
78            try {
79                removeRecyclerView();
80            } catch (Throwable throwable) {
81                throwable.printStackTrace();
82            }
83        }
84        getInstrumentation().waitForIdleSync();
85        super.tearDown();
86    }
87
88    public void removeRecyclerView() throws Throwable {
89        mRecyclerView = null;
90        runTestOnUiThread(new Runnable() {
91            @Override
92            public void run() {
93                getActivity().mContainer.removeAllViews();
94            }
95        });
96    }
97
98    public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
99        mRecyclerView = recyclerView;
100        mAdapterHelper = recyclerView.mAdapterHelper;
101        runTestOnUiThread(new Runnable() {
102            @Override
103            public void run() {
104                getActivity().mContainer.addView(recyclerView);
105            }
106        });
107    }
108
109    public void requestLayoutOnUIThread(final View view) throws Throwable {
110        runTestOnUiThread(new Runnable() {
111            @Override
112            public void run() {
113                view.requestLayout();
114            }
115        });
116    }
117
118    public void scrollBy(final int dt) throws Throwable {
119        runTestOnUiThread(new Runnable() {
120            @Override
121            public void run() {
122                if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
123                    mRecyclerView.scrollBy(dt, 0);
124                } else {
125                    mRecyclerView.scrollBy(0, dt);
126                }
127
128            }
129        });
130    }
131
132    void scrollToPosition(final int position) throws Throwable {
133        runTestOnUiThread(new Runnable() {
134            @Override
135            public void run() {
136                mRecyclerView.getLayoutManager().scrollToPosition(position);
137            }
138        });
139    }
140
141    void smoothScrollToPosition(final int position)
142            throws Throwable {
143        Log.d(TAG, "SMOOTH scrolling to " + position);
144        runTestOnUiThread(new Runnable() {
145            @Override
146            public void run() {
147                mRecyclerView.smoothScrollToPosition(position);
148            }
149        });
150        while (mRecyclerView.getLayoutManager().isSmoothScrolling() ||
151                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
152            if (mDebug) {
153                Log.d(TAG, "SMOOTH scrolling step");
154            }
155            Thread.sleep(200);
156        }
157        Log.d(TAG, "SMOOTH scrolling done");
158    }
159
160    class TestViewHolder extends RecyclerView.ViewHolder {
161
162        Item mBindedItem;
163
164        public TestViewHolder(View itemView) {
165            super(itemView);
166        }
167    }
168
169    class TestLayoutManager extends RecyclerView.LayoutManager {
170
171        CountDownLatch layoutLatch;
172
173        public void expectLayouts(int count) {
174            layoutLatch = new CountDownLatch(count);
175        }
176
177        public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable {
178            layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit);
179            assertEquals("all expected layouts should be executed at the expected time",
180                    0, layoutLatch.getCount());
181        }
182
183        public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
184            layoutLatch.await(timeout, TimeUnit.SECONDS);
185            assertEquals(msg, count, layoutLatch.getCount());
186        }
187
188        public void assertNoLayout(String msg, long timeout) throws Throwable {
189            layoutLatch.await(timeout, TimeUnit.SECONDS);
190            assertFalse(msg, layoutLatch.getCount() == 0);
191        }
192
193        public void waitForLayout(long timeout) throws Throwable {
194            waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS);
195        }
196
197        @Override
198        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
199            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
200                    ViewGroup.LayoutParams.WRAP_CONTENT);
201        }
202
203        void assertVisibleItemPositions() {
204            int i = getChildCount();
205            TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter();
206            while (i-- > 0) {
207                View view = getChildAt(i);
208                RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
209                Item item = ((TestViewHolder) lp.mViewHolder).mBindedItem;
210                if (mDebug) {
211                    Log.d(TAG, "testing item " + i);
212                }
213                if (!lp.isItemRemoved()) {
214                    RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view);
215                    assertSame("item position in LP should match adapter value :" + vh,
216                            testAdapter.mItems.get(vh.mPosition), item);
217                }
218            }
219        }
220
221        RecyclerView.LayoutParams getLp(View v) {
222            return (RecyclerView.LayoutParams) v.getLayoutParams();
223        }
224
225        /**
226         * returns skipped (removed) view count.
227         */
228        int layoutRange(RecyclerView.Recycler recycler, int start,
229                int end) {
230            int skippedAdd = 0;
231            if (mDebug) {
232                Log.d(TAG, "will layout items from " + start + " to " + end);
233            }
234            for (int i = start; i < end; i++) {
235                if (mDebug) {
236                    Log.d(TAG, "laying out item " + i);
237                }
238                View view = recycler.getViewForPosition(i);
239                assertNotNull("view should not be null for valid position. "
240                        + "got null view at position " + i, view);
241                if (!getLp(view).isItemRemoved()) {
242                    addView(view);
243                } else {
244                    skippedAdd ++;
245                }
246
247                measureChildWithMargins(view, 0, 0);
248                layoutDecorated(view, 0, (i - start) * 10, getDecoratedMeasuredWidth(view)
249                        , getDecoratedMeasuredHeight(view));
250            }
251            return skippedAdd;
252        }
253    }
254
255    static class Item {
256        final static AtomicInteger idCounter = new AtomicInteger(0);
257        final public int mId = idCounter.incrementAndGet();
258
259        int originalIndex;
260
261        final String text;
262
263        Item(int originalIndex, String text) {
264            this.originalIndex = originalIndex;
265            this.text = text;
266        }
267
268        @Override
269        public String toString() {
270            return "Item{" +
271                    "mId=" + mId +
272                    ", originalIndex=" + originalIndex +
273                    ", text='" + text + '\'' +
274                    '}';
275        }
276    }
277
278    class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
279
280        List<Item> mItems;
281
282        TestAdapter(int count) {
283            mItems = new ArrayList<Item>(count);
284            for (int i = 0; i < count; i++) {
285                mItems.add(new Item(i, "Item " + i));
286            }
287        }
288
289        @Override
290        public TestViewHolder onCreateViewHolder(ViewGroup parent,
291                int viewType) {
292            return new TestViewHolder(new TextView(parent.getContext()));
293        }
294
295        @Override
296        public void onBindViewHolder(TestViewHolder holder, int position) {
297            final Item item = mItems.get(position);
298            ((TextView) (holder.itemView)).setText(item.text);
299            holder.mBindedItem = item;
300        }
301
302        public void deleteAndNotify(final int start, final int count) throws Throwable {
303            deleteAndNotify(new int[]{start, count});
304        }
305
306        /**
307         * Deletes items in the given ranges.
308         * <p>
309         * Note that each operation affects the one after so you should offset them properly.
310         * <p>
311         * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with
312         * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be
313         * A D E. Then it will delete 2,1 which means it will delete E.
314         */
315        public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
316            for (int[] tuple : startCountTuples) {
317                tuple[1] = -tuple[1];
318            }
319            new AddRemoveRunnable(startCountTuples).runOnMainThread();
320        }
321
322        public void addAndNotify(final int start, final int count) throws Throwable {
323            addAndNotify(new int[]{start, count});
324        }
325
326        public void addAndNotify(final int[]... startCountTuples) throws Throwable {
327            new AddRemoveRunnable(startCountTuples).runOnMainThread();
328        }
329
330        public void notifyChange() throws Throwable {
331            runTestOnUiThread(new Runnable() {
332                @Override
333                public void run() {
334                    notifyDataSetChanged();
335                }
336            });
337        }
338
339        public void notifyItemChange(final int start, final int count) throws Throwable {
340            runTestOnUiThread(new Runnable() {
341                @Override
342                public void run() {
343                    notifyItemRangeChanged(start, count);
344                }
345            });
346        }
347
348        /**
349         * Similar to other methods but negative count means delete and position count means add.
350         * <p>
351         * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an
352         * item to index 1, then remove an item from index 2 (updated index 2)
353         */
354        public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable {
355            new AddRemoveRunnable(startCountTuples).runOnMainThread();
356        }
357
358        @Override
359        public int getItemCount() {
360            return mItems.size();
361        }
362
363
364        private class AddRemoveRunnable implements Runnable {
365            final int[][] mStartCountTuples;
366
367            public AddRemoveRunnable(int[][] startCountTuples) {
368                mStartCountTuples = startCountTuples;
369            }
370
371            public void runOnMainThread() throws Throwable {
372                if (Looper.myLooper() == Looper.getMainLooper()) {
373                    run();
374                } else {
375                    runTestOnUiThread(this);
376                }
377            }
378
379            @Override
380            public void run() {
381                for (int[] tuple : mStartCountTuples) {
382                    if (tuple[1] < 0) {
383                        delete(tuple);
384                    } else {
385                        add(tuple);
386                    }
387                }
388            }
389
390            private void add(int[] tuple) {
391                for (int i = 0; i < tuple[1]; i++) {
392                    mItems.add(tuple[0], new Item(i, "new item " + i));
393                }
394                // offset others
395                for (int i = tuple[0] + tuple[1]; i < mItems.size(); i++) {
396                    mItems.get(i).originalIndex += tuple[1];
397                }
398                notifyItemRangeInserted(tuple[0], tuple[1]);
399            }
400
401            private void delete(int[] tuple) {
402                for (int i = 0; i < -tuple[1]; i++) {
403                    mItems.remove(tuple[0]);
404                }
405                notifyItemRangeRemoved(tuple[0], -tuple[1]);
406            }
407        }
408    }
409
410    @Override
411    public void runTestOnUiThread(Runnable r) throws Throwable {
412        if (Looper.myLooper() == Looper.getMainLooper()) {
413            r.run();
414        } else {
415            super.runTestOnUiThread(r);
416        }
417    }
418}