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