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}