1/*
2 * Copyright (C) 2017 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 androidx.leanback.widget;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertTrue;
21import static org.mockito.ArgumentMatchers.any;
22import static org.mockito.ArgumentMatchers.anyInt;
23import static org.mockito.ArgumentMatchers.eq;
24import static org.mockito.Mockito.atLeast;
25import static org.mockito.Mockito.never;
26import static org.mockito.Mockito.times;
27
28import android.content.Context;
29import android.os.Bundle;
30import android.support.test.InstrumentationRegistry;
31import android.support.test.filters.SmallTest;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.FrameLayout;
35
36import androidx.annotation.Nullable;
37import androidx.leanback.R;
38import androidx.recyclerview.widget.RecyclerView;
39
40import org.junit.Before;
41import org.junit.Test;
42import org.junit.runner.RunWith;
43import org.junit.runners.JUnit4;
44import org.mockito.Mockito;
45
46import java.util.ArrayList;
47import java.util.List;
48
49@SmallTest
50@RunWith(JUnit4.class)
51public class ObjectAdapterTest {
52
53    private static final String ID = "id";
54    private static final String STRING_MEMBER_ONE = "stringMemberOne";
55    private static final String STRING_MEMBER_TWO = "stringMemberTwo";
56    private static final String NOT_RELATED_STRING_MEMBER = "notRelatedStringMember";
57
58    protected ItemBridgeAdapter mBridgeAdapter;
59    protected ArrayObjectAdapter mAdapter;
60
61    private ArrayList mItems;
62    private DiffCallback<AdapterItem> mMockedCallback;
63    private DiffCallback<AdapterItem> mCallbackWithoutPayload;
64    private RecyclerView.AdapterDataObserver mObserver;
65
66    private Context mContext;
67    private ListRowPresenter mListRowPresenter;
68    private ListRowPresenter.ViewHolder mListVh;
69    private ArrayObjectAdapter mRowsAdapter;
70    private AdapterItemPresenter mAdapterItemPresenter;
71
72    private ListRow mRow;
73
74    /**
75     * This type is used to test setItems() API.
76     */
77    private static class AdapterItem {
78        private int mId;
79        private String mStringMemberOne;
80
81        // mStringMemberTwo is only used to test if correct payload can be generated.
82        private String mStringMemberTwo;
83
84        // not related string will not impact the result of our equals function.
85        // Used to verify if payload computing process still honor the rule set by
86        // areContentsTheSame() method
87        private String mNotRelatedStringMember;
88
89        AdapterItem(int id, String stringMemberOne) {
90            mId = id;
91            mStringMemberOne = stringMemberOne;
92            mStringMemberTwo = "";
93            mNotRelatedStringMember = "";
94        }
95
96        AdapterItem(int id, String stringMemberOne, String stringMemberTwo) {
97            mId = id;
98            mStringMemberOne = stringMemberOne;
99            mStringMemberTwo = stringMemberTwo;
100            mNotRelatedStringMember = "";
101        }
102
103        AdapterItem(int id, String stringMemberOne, String stringMemberTwo,
104                String notRelatedStringMember) {
105            mId = id;
106            mStringMemberOne = stringMemberOne;
107            mStringMemberTwo = stringMemberTwo;
108            mNotRelatedStringMember = notRelatedStringMember;
109        }
110
111        public int getId() {
112            return mId;
113        }
114
115        public String getStringMemberOne() {
116            return mStringMemberOne;
117        }
118
119        public String getStringMemberTwo() {
120            return mStringMemberTwo;
121        }
122
123        public String getNotRelatedStringMember() {
124            return mNotRelatedStringMember;
125        }
126
127        @Override
128        public boolean equals(Object o) {
129            if (this == o) return true;
130            if (o == null || getClass() != o.getClass()) return false;
131
132            AdapterItem that = (AdapterItem) o;
133
134            if (mId != that.mId) return false;
135            if (mStringMemberOne != null ? !mStringMemberOne.equals(that.mStringMemberOne)
136                    : that.mStringMemberOne != null) {
137                return false;
138            }
139            return mStringMemberTwo != null ? mStringMemberTwo.equals(that.mStringMemberTwo)
140                    : that.mStringMemberTwo == null;
141        }
142
143        @Override
144        public int hashCode() {
145            int result = mId;
146            result = 31 * result + (mStringMemberOne != null ? mStringMemberOne.hashCode() : 0);
147            result = 31 * result + (mStringMemberTwo != null ? mStringMemberTwo.hashCode() : 0);
148            return result;
149        }
150    }
151
152    /**
153     * Extend from DiffCallback extended class to define the rule to compare if two items are the
154     * same/ have the same content and how to calculate the payload.
155     *
156     * The payload will only be calculated when the two items are the same but with different
157     * contents. So we make this class as a public class which can be mocked by mockito to verify
158     * if the calculation process satisfies our requirement.
159     */
160    public static class DiffCallbackPayloadTesting extends DiffCallback<AdapterItem> {
161        // Using item's mId as the standard to judge if two items is the same
162        @Override
163        public boolean areItemsTheSame(AdapterItem oldItem, AdapterItem newItem) {
164            return oldItem.getId() == newItem.getId();
165        }
166
167        // Using equals method to judge if two items have the same content.
168        @Override
169        public boolean areContentsTheSame(AdapterItem oldItem, AdapterItem newItem) {
170            return oldItem.equals(newItem);
171        }
172
173        @Nullable
174        @Override
175        public Object getChangePayload(AdapterItem oldItem,
176                AdapterItem newItem) {
177            Bundle diff = new Bundle();
178            if (oldItem.getId() != newItem.getId()) {
179                diff.putInt(ID, newItem.getId());
180            }
181
182            if (!oldItem.getStringMemberOne().equals(newItem.getStringMemberOne())) {
183                diff.putString(STRING_MEMBER_ONE, newItem.getStringMemberOne());
184            }
185
186            if (!oldItem.getStringMemberTwo().equals(newItem.getStringMemberTwo())) {
187                diff.putString(STRING_MEMBER_TWO, newItem.getStringMemberTwo());
188            }
189
190            if (!oldItem.getNotRelatedStringMember().equals(newItem.getNotRelatedStringMember())) {
191                diff.putString(NOT_RELATED_STRING_MEMBER, newItem.getNotRelatedStringMember());
192            }
193
194            if (diff.size() == 0) {
195                return null;
196            }
197            return diff;
198        }
199    }
200
201    /**
202     * The presenter designed for adapter item.
203     *
204     * The reason to set this class as a public class is for Mockito to mock it. So we can observe
205     * method's dispatching easily
206     */
207    public static class AdapterItemPresenter extends Presenter {
208        int mWidth;
209        int mHeight;
210
211        AdapterItemPresenter() {
212            this(100, 100);
213        }
214
215        AdapterItemPresenter(int width, int height) {
216            mWidth = width;
217            mHeight = height;
218        }
219
220        @Override
221        public ViewHolder onCreateViewHolder(ViewGroup parent) {
222            View view = new View(parent.getContext());
223            view.setFocusable(true);
224            view.setId(R.id.lb_action_button);
225            view.setLayoutParams(new ViewGroup.LayoutParams(mWidth, mHeight));
226            return new Presenter.ViewHolder(view);
227        }
228
229        @Override
230        public void onBindViewHolder(ViewHolder viewHolder, Object item) {
231            // no - op
232        }
233
234        @Override
235        public void onUnbindViewHolder(ViewHolder viewHolder) {
236            // no - op
237        }
238
239        @Override
240        public void onBindViewHolder(ViewHolder viewHolder, Object item,
241                List<Object> payloads) {
242            // no - op
243        }
244    }
245
246
247    /**
248     * Initialize test-related members.
249     */
250    @Before
251    public void setup() {
252        mAdapter = new ArrayObjectAdapter();
253        mBridgeAdapter = new ItemBridgeAdapter(mAdapter);
254        mItems = new ArrayList();
255        mMockedCallback = Mockito.spy(DiffCallbackPayloadTesting.class);
256
257        // the diff callback without calculating the payload
258        mCallbackWithoutPayload = new DiffCallback<AdapterItem>() {
259
260            // Using item's mId as the standard to judge if two items is the same
261            @Override
262            public boolean areItemsTheSame(AdapterItem oldItem, AdapterItem newItem) {
263                return oldItem.getId() == newItem.getId();
264            }
265
266            // Using equals method to judge if two items have the same content.
267            @Override
268            public boolean areContentsTheSame(AdapterItem oldItem, AdapterItem newItem) {
269                return oldItem.equals(newItem);
270            }
271        };
272
273        // Spy the RecyclerView.AdapterObserver
274        mObserver = Mockito.spy(RecyclerView.AdapterDataObserver.class);
275
276        // register observer so we can observe the events
277        mBridgeAdapter.registerAdapterDataObserver(mObserver);
278
279        // obtain context through instrumentation registry
280        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
281
282        //
283        ListRowPresenter listRowPresenter = new ListRowPresenter();
284        mListRowPresenter = Mockito.spy(listRowPresenter);
285
286        // mock item presenter
287        AdapterItemPresenter adapterItemPresenter = new AdapterItemPresenter();
288        mAdapterItemPresenter = Mockito.spy(adapterItemPresenter);
289        mRow = new ListRow(new ArrayObjectAdapter(mAdapterItemPresenter));
290    }
291
292    /**
293     * The following test case is mainly focused on the basic functionality provided by
294     * Object Adapter.
295     *
296     * The key purpose for this test is to make sure when adapter send out a signal through
297     * notify function, it will finally be intercepted by recycler view's observer
298     */
299    @Test
300    public void testBasicFunctionality() {
301        mItems.add("a");
302        mItems.add("b");
303        mItems.add("c");
304        mAdapter.addAll(0, mItems);
305
306        // size
307        assertEquals(mAdapter.size(), 3);
308
309        // get
310        assertEquals(mAdapter.get(0), "a");
311        assertEquals(mAdapter.get(1), "b");
312        assertEquals(mAdapter.get(2), "c");
313
314        // indexOf
315        assertEquals(mAdapter.indexOf("a"), 0);
316        assertEquals(mAdapter.indexOf("b"), 1);
317        assertEquals(mAdapter.indexOf("c"), 2);
318
319        // insert
320        mAdapter.add(1, "a1");
321        Mockito.verify(mObserver).onItemRangeInserted(1, 1);
322        assertAdapterContent(mAdapter, new Object[]{"a", "a1", "b", "c"});
323        Mockito.reset(mObserver);
324
325        // insert multiple
326        ArrayList newItems1 = new ArrayList();
327        newItems1.add("a2");
328        newItems1.add("a3");
329        mAdapter.addAll(1, newItems1);
330        Mockito.verify(mObserver).onItemRangeInserted(1, 2);
331        assertAdapterContent(mAdapter, new Object[]{"a", "a2", "a3", "a1", "b", "c"});
332        Mockito.reset(mObserver);
333
334        // update
335        mAdapter.notifyArrayItemRangeChanged(2, 3);
336        Mockito.verify(mObserver).onItemRangeChanged(2, 3);
337        assertAdapterContent(mAdapter, new Object[]{"a", "a2", "a3", "a1", "b", "c"});
338        Mockito.reset(mObserver);
339
340        // remove
341        mAdapter.removeItems(1, 4);
342        Mockito.verify(mObserver).onItemRangeRemoved(1, 4);
343        assertAdapterContent(mAdapter, new Object[]{"a", "c"});
344        Mockito.reset(mObserver);
345
346        // move
347        mAdapter.move(0, 1);
348        Mockito.verify(mObserver).onItemRangeMoved(0, 1, 1);
349        assertAdapterContent(mAdapter, new Object[]{"c", "a"});
350        Mockito.reset(mObserver);
351
352        // replace
353        mAdapter.replace(0, "a");
354        Mockito.verify(mObserver).onItemRangeChanged(0, 1);
355        assertAdapterContent(mAdapter, new Object[]{"a", "a"});
356        Mockito.reset(mObserver);
357        mAdapter.replace(1, "b");
358        Mockito.verify(mObserver).onItemRangeChanged(1, 1);
359        assertAdapterContent(mAdapter, new Object[]{"a", "b"});
360        Mockito.reset(mObserver);
361
362        // remove multiple
363        mItems.clear();
364        mItems.add("a");
365        mItems.add("b");
366        mAdapter.addAll(0, mItems);
367        mAdapter.removeItems(0, 2);
368        Mockito.verify(mObserver).onItemRangeRemoved(0, 2);
369        assertAdapterContent(mAdapter, new Object[]{"a", "b"});
370        Mockito.reset(mObserver);
371
372        // clear
373        mAdapter.clear();
374        Mockito.verify(mObserver).onItemRangeRemoved(0, 2);
375        assertAdapterContent(mAdapter, new Object[]{});
376        Mockito.reset(mObserver);
377
378        // isImmediateNotifySupported
379        assertTrue(mAdapter.isImmediateNotifySupported());
380    }
381
382
383    @Test
384    public void testSetItemsNoDiffCallback() {
385        mItems.add(new AdapterItem(1, "a"));
386        mItems.add(new AdapterItem(2, "b"));
387        mItems.add(new AdapterItem(3, "c"));
388        mAdapter.setItems(mItems, null);
389        Mockito.verify(mObserver, times(1)).onChanged();
390        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
391        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
392        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
393
394        mItems.add(new AdapterItem(4, "a"));
395        mItems.add(new AdapterItem(5, "b"));
396        mItems.add(new AdapterItem(6, "c"));
397        mAdapter.setItems(mItems, null);
398        Mockito.verify(mObserver, times(2)).onChanged();
399        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
400        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
401        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
402    }
403
404    /**
405     * The following test cases are mainly focused on the basic functionality provided by setItems
406     * function
407     *
408     * It can be deemed as an extension to the previous test, and won't consider payload in this
409     * test case.
410     *
411     * Test0 will treat all items as the same item with same content.
412     */
413    @Test
414    public void testSetItemsMethod0() {
415        mItems.add("a");
416        mItems.add("b");
417        mItems.add("c");
418
419        DiffCallback<String> callback = new DiffCallback<String>() {
420
421            // Always treat two items are the same.
422            @Override
423            public boolean areItemsTheSame(String oldItem, String newItem) {
424                return true;
425            }
426
427            // Always treat two items have the same content.
428            @Override
429            public boolean areContentsTheSame(String oldItem, String newItem) {
430                return true;
431            }
432        };
433
434        mAdapter.setItems(mItems, callback);
435        // verify method dispatching
436        Mockito.verify(mObserver).onItemRangeInserted(0, 3);
437
438        // Clear previous items and set a new list of items.
439        mItems.clear();
440        mItems.add("a");
441        mItems.add("b");
442        mItems.add("c");
443
444        // reset mocked object before calling setItems method
445        Mockito.reset(mObserver);
446        mAdapter.setItems(mItems, callback);
447
448        // verify method dispatching
449        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
450        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
451        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
452        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
453        assertAdapterContent(mAdapter, new Object[]{"a", "b", "c"});
454    }
455
456    /**
457     * Test1 will treat all items as the same item with same content.
458     */
459    @Test
460    public void testSetItemsMethod1() {
461        mItems.add("a");
462        mItems.add("b");
463        mItems.add("c");
464
465        DiffCallback<String> callback = new DiffCallback<String>() {
466
467            // Always treat two items are the different.
468            @Override
469            public boolean areItemsTheSame(String oldItem, String newItem) {
470                return false;
471            }
472
473            // Always treat two items have the different content.
474            @Override
475            public boolean areContentsTheSame(String oldItem, String newItem) {
476                return false;
477            }
478        };
479
480        mAdapter.setItems(mItems, callback);
481        // verify method dispatching
482        Mockito.verify(mObserver).onItemRangeInserted(0, 3);
483
484        // Clear previous items and set a new list of items.
485        mItems.clear();
486        mItems.add("a");
487        mItems.add("b");
488        mItems.add("c");
489
490        // reset mocked object before calling setItems method
491        Mockito.reset(mObserver);
492        mAdapter.setItems(mItems, callback);
493
494        // No change or move event should be fired under current callback.
495        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
496        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
497        Mockito.verify(mObserver).onItemRangeRemoved(0, 3);
498        Mockito.verify(mObserver).onItemRangeInserted(0, 3);
499        assertAdapterContent(mAdapter, new Object[]{"a", "b", "c"});
500    }
501
502    /**
503     * Test2 will trigger notifyItemRangeChanged event
504     */
505    @Test
506    public void testSetItemsMethod2() {
507        // initial item list
508        mItems.add(new AdapterItem(1, "a"));
509        mItems.add(new AdapterItem(2, "b"));
510        mItems.add(new AdapterItem(3, "c"));
511        mAdapter.setItems(mItems, mCallbackWithoutPayload);
512
513        // Clear previous items and set a new list of items.
514        mItems.clear();
515        mItems.add(new AdapterItem(1, "a"));
516        mItems.add(new AdapterItem(2, "c"));
517        mItems.add(new AdapterItem(3, "b"));
518
519        // reset mocked object before calling setItems method
520        Mockito.reset(mObserver);
521        mAdapter.setItems(mItems, mCallbackWithoutPayload);
522
523        // verify method dispatching
524        Mockito.verify(mObserver).onItemRangeChanged(1, 2, null);
525    }
526
527
528    /**
529     * Test3 will trigger notifyItemMoved event
530     */
531    @Test
532    public void testSetItemsMethod3() {
533        // initial item list
534        mItems.add(new AdapterItem(1, "a"));
535        mItems.add(new AdapterItem(2, "b"));
536        mItems.add(new AdapterItem(3, "c"));
537        mAdapter.setItems(mItems, mCallbackWithoutPayload);
538
539        // Clear previous items and set a new list of items.
540        mItems.clear();
541        mItems.add(new AdapterItem(2, "b"));
542        mItems.add(new AdapterItem(1, "a"));
543        mItems.add(new AdapterItem(3, "c"));
544
545        // reset mocked object before calling setItems method
546        Mockito.reset(mObserver);
547        mAdapter.setItems(mItems, mCallbackWithoutPayload);
548
549        // verify method dispatching
550        Mockito.verify(mObserver).onItemRangeMoved(1, 0, 1);
551    }
552
553    /**
554     * Test4 will trigger notifyItemRangeRemoved event
555     */
556    @Test
557    public void testSetItemsMethod4() {
558        // initial item list
559        mItems.add(new AdapterItem(1, "a"));
560        mItems.add(new AdapterItem(2, "b"));
561        mItems.add(new AdapterItem(3, "c"));
562        mAdapter.setItems(mItems, mCallbackWithoutPayload);
563
564        // Clear previous items and set a new list of items.
565        mItems.clear();
566        mItems.add(new AdapterItem(2, "b"));
567        mItems.add(new AdapterItem(3, "c"));
568
569        // reset mocked object before calling setItems method
570        Mockito.reset(mObserver);
571        mAdapter.setItems(mItems, mCallbackWithoutPayload);
572
573        // verify method dispatching
574        Mockito.verify(mObserver).onItemRangeRemoved(0, 1);
575    }
576
577    /**
578     * Test5 will trigger notifyItemRangeInserted event
579     */
580    @Test
581    public void testSetItemsMethod5() {
582        // initial item list
583        mItems.add(new AdapterItem(1, "a"));
584        mItems.add(new AdapterItem(2, "b"));
585        mItems.add(new AdapterItem(3, "c"));
586        mAdapter.setItems(mItems, mCallbackWithoutPayload);
587
588        // Clear previous items and set a new list of items.
589        mItems.clear();
590        mItems.add(new AdapterItem(1, "a"));
591        mItems.add(new AdapterItem(2, "b"));
592        mItems.add(new AdapterItem(3, "c"));
593        mItems.add(new AdapterItem(4, "d"));
594
595        // reset mocked object before calling setItems method
596        Mockito.reset(mObserver);
597        mAdapter.setItems(mItems, mCallbackWithoutPayload);
598
599        // verify method dispatching
600        Mockito.verify(mObserver).onItemRangeInserted(3, 1);
601    }
602
603
604    /**
605     * Test6 will trigger notifyItemRangeInserted event and notifyItemRangeRemoved event
606     * simultaneously
607     */
608    @Test
609    public void testSetItemsMethod6() {
610        // initial item list
611        mItems.add(new AdapterItem(1, "a"));
612        mItems.add(new AdapterItem(2, "b"));
613        mItems.add(new AdapterItem(3, "c"));
614        mAdapter.setItems(mItems, mCallbackWithoutPayload);
615
616        // Clear previous items and set a new list of items.
617        mItems.clear();
618        mItems.add(new AdapterItem(2, "a"));
619        mItems.add(new AdapterItem(2, "b"));
620        mItems.add(new AdapterItem(3, "c"));
621
622        // reset mocked object before calling setItems method
623        Mockito.reset(mObserver);
624        mAdapter.setItems(mItems, mCallbackWithoutPayload);
625
626        // verify method dispatching
627        Mockito.verify(mObserver).onItemRangeRemoved(0, 1);
628        Mockito.verify(mObserver).onItemRangeInserted(0, 1);
629    }
630
631    /**
632     * Test7 will trigger notifyItemRangeMoved and notifyItemRangeChanged event simultaneously
633     */
634    @Test
635    public void testItemsMethod7() {
636        // initial item list
637        mItems.add(new AdapterItem(1, "a"));
638        mItems.add(new AdapterItem(2, "b"));
639        mItems.add(new AdapterItem(3, "c"));
640        mAdapter.setItems(mItems, mCallbackWithoutPayload);
641
642        // Clear previous items and set a new list of items.
643        mItems.clear();
644        mItems.add(new AdapterItem(1, "aa"));
645        mItems.add(new AdapterItem(3, "c"));
646        mItems.add(new AdapterItem(2, "b"));
647
648        // reset mocked object before calling setItems method
649        Mockito.reset(mObserver);
650        mAdapter.setItems(mItems, mCallbackWithoutPayload);
651
652        // verify method dispatching
653        Mockito.verify(mObserver).onItemRangeChanged(0, 1, null);
654        Mockito.verify(mObserver).onItemRangeMoved(2, 1, 1);
655    }
656
657    /**
658     * Test8 will trigger multiple items insertion event
659     */
660    @Test
661    public void testSetItemsMethod8() {
662
663        // initial item list
664        mAdapter.clear();
665        mItems.add(new AdapterItem(0, "a"));
666        mItems.add(new AdapterItem(1, "b"));
667        mAdapter.clear();
668        mAdapter.setItems(mItems, mCallbackWithoutPayload);
669
670        // Clear previous items and set a new list of items.
671        mItems.clear();
672        mItems.add(new AdapterItem(0, "a"));
673        mItems.add(new AdapterItem(1, "b"));
674        mItems.add(new AdapterItem(2, "c"));
675        mItems.add(new AdapterItem(3, "d"));
676
677        // reset mocked object before calling setItems method
678        Mockito.reset(mObserver);
679        mAdapter.setItems(mItems, mCallbackWithoutPayload);
680
681        // verify method dispatching
682        Mockito.verify(mObserver).onItemRangeInserted(2, 2);
683        Mockito.reset(mObserver);
684    }
685
686
687    /**
688     * The following test cases are mainly focused on testing setItems method when we need to
689     * calculate payload
690     *
691     * The payload should only be calculated when two items are same but with different contents.
692     * I.e. the calculate payload method should only be executed when the previous condition is
693     * satisfied. In this test case we use a mocked callback object to verify it and compare the
694     * calculated payload with our expected payload.
695     *
696     * Test 0 will calculate the difference on string member one.
697     */
698    @Test
699    public void testPayloadCalculation0() {
700        AdapterItem oldItem = new AdapterItem(1, "a", "a");
701        mItems.add(oldItem);
702        mAdapter.setItems(mItems, mMockedCallback);
703
704        // Create a new item list which contain a new AdapterItem object
705        // test if payload is computed correctly by changing string member 1
706        mItems.clear();
707        AdapterItem newItem = new AdapterItem(1, "aa", "a");
708        mItems.add(newItem);
709
710
711        // reset mocked object before calling setItems method
712        Mockito.reset(mObserver);
713        Mockito.reset(mMockedCallback);
714        mAdapter.setItems(mItems, mMockedCallback);
715
716        // Create expected payload manually for verification
717        Bundle expectedPayload0 = new Bundle();
718        expectedPayload0.putString(STRING_MEMBER_ONE, newItem.getStringMemberOne());
719
720        // make sure no other event will be triggered in current scenario
721        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
722        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
723        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
724
725        // Check if getChangePayload is executed as we expected
726        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), eq(null));
727        Mockito.verify(mMockedCallback).getChangePayload(oldItem,
728                newItem);
729
730        // compare the two bundles by iterating each member
731        Bundle calculatedBundle0 = (Bundle) mMockedCallback.getChangePayload(
732                oldItem, newItem);
733        compareTwoBundles(calculatedBundle0, expectedPayload0);
734
735    }
736
737    /**
738     * Test 1 will calculate the difference on string member two.
739     */
740    @Test
741    public void testPayloadComputation1() {
742        AdapterItem oldItem = new AdapterItem(1, "a", "a");
743        mItems.add(oldItem);
744        mAdapter.setItems(mItems, mMockedCallback);
745
746        // Create a new item list which contain a new AdapterItem object
747        // test if payload is computed correctly by changing string member 2
748        mItems.clear();
749        AdapterItem newItem = new AdapterItem(1, "a", "aa");
750        mItems.add(newItem);
751
752        // reset mocked object before calling setItems method
753        Mockito.reset(mObserver);
754        Mockito.reset(mMockedCallback);
755        mAdapter.setItems(mItems, mMockedCallback);
756
757        // Create expected payload manually for verification
758        Bundle expectedPayload0 = new Bundle();
759        expectedPayload0.putString(STRING_MEMBER_TWO, newItem.getStringMemberTwo());
760
761        // make sure no other event will be triggered in current scenario
762        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
763        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
764        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
765
766        // Check if getChangePayload is executed as we expected
767        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), eq(null));
768        Mockito.verify(mMockedCallback).getChangePayload(oldItem,
769                newItem);
770
771        // compare the two bundles by iterating each member
772        Bundle calculatedBundle0 = (Bundle) mMockedCallback.getChangePayload(
773                oldItem, newItem);
774        compareTwoBundles(calculatedBundle0, expectedPayload0);
775
776    }
777
778    /**
779     * Test 1 will calculate the difference on string member one and string member two.
780     */
781    @Test
782    public void testPayloadComputation2() {
783        AdapterItem oldItem = new AdapterItem(1, "a", "a");
784        mItems.add(oldItem);
785        mAdapter.setItems(mItems, mMockedCallback);
786
787        // Create a new item list which contain a new AdapterItem object
788        // test if payload is computed correctly by changing string member 1 and string member 2
789        mItems.clear();
790        AdapterItem newItem = new AdapterItem(1, "aa", "aa");
791        mItems.add(newItem);
792
793        // reset mocked object before calling setItems method
794        Mockito.reset(mObserver);
795        Mockito.reset(mMockedCallback);
796        mAdapter.setItems(mItems, mMockedCallback);
797
798        // Create expected payload manually for verification
799        Bundle expectedPayload0 = new Bundle();
800        expectedPayload0.putString(STRING_MEMBER_ONE, newItem.getStringMemberOne());
801        expectedPayload0.putString(STRING_MEMBER_TWO, newItem.getStringMemberTwo());
802
803        // make sure no other event will be triggered in current scenario
804        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
805        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
806        Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt());
807
808        // Check if getChangePayload is executed as we expected
809        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), eq(null));
810        Mockito.verify(mMockedCallback).getChangePayload(oldItem,
811                newItem);
812
813        // compare the two bundles by iterating each member
814        Bundle calculatedBundle0 = (Bundle) mMockedCallback.getChangePayload(
815                oldItem, newItem);
816        compareTwoBundles(calculatedBundle0, expectedPayload0);
817
818    }
819
820    /**
821     * Test payload computation process under the condition when two items are not the same
822     * based on areItemsTheSame function in DiffUtilCallback
823     */
824    @Test
825    public void testPayloadComputationNewItem0() {
826        AdapterItem oldItem = new AdapterItem(1, "a", "a");
827        mItems.add(oldItem);
828        mAdapter.setItems(mItems, mMockedCallback);
829
830        // Create a new item list which contain a new AdapterItem object
831        // The id of the new item is changed, and will be treated as a new item according to the
832        // rule we set in the callback. This test case is to verify the getChangePayload
833        // method still honor the standard we set up to judge new item
834        mItems.clear();
835        AdapterItem newItem = new AdapterItem(2, "a", "a");
836        mItems.add(newItem);
837
838        // reset mocked object before calling setItems method
839        Mockito.reset(mObserver);
840        Mockito.reset(mMockedCallback);
841        mAdapter.setItems(mItems, mMockedCallback);
842
843        // Make sure only remove/ insert event will be fired under this circumstance
844        Mockito.verify(mObserver).onItemRangeRemoved(0, 1);
845        Mockito.verify(mObserver).onItemRangeInserted(0, 1);
846        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
847        Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(),
848                (AdapterItem) any());
849
850    }
851
852    /**
853     * Test payload computation process under the condition when two items are not the same
854     * based on areItemsTheSame function in DiffUtilCallback
855     *
856     * But in test 1 we have changed string member one for sanity check.
857     */
858    @Test
859    public void testPayloadComputationNewItem1() {
860        AdapterItem oldItem = new AdapterItem(1, "a", "a");
861        mItems.add(oldItem);
862        mAdapter.setItems(mItems, mMockedCallback);
863
864        // Create a new item list which contain a new AdapterItem object
865        // The id of the new item is changed, and will be treated as a new item according to the
866        // rule we set in the callback. This test case is to verify the getChangePayload
867        // method still honor the standard we set up to judge new item
868        mItems.clear();
869        AdapterItem newItem = new AdapterItem(2, "aa", "a");
870        mItems.add(newItem);
871
872        // reset mocked object before calling setItems method
873        Mockito.reset(mObserver);
874        Mockito.reset(mMockedCallback);
875        mAdapter.setItems(mItems, mMockedCallback);
876
877        // Make sure only remove/ insert event will be fired under this circumstance
878        Mockito.verify(mObserver).onItemRangeRemoved(0, 1);
879        Mockito.verify(mObserver).onItemRangeInserted(0, 1);
880        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
881        Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(),
882                (AdapterItem) any());
883
884    }
885
886    /**
887     * Test payload computation process under the condition when two items are not the same
888     * based on areItemsTheSame function in DiffUtilCallback
889     *
890     * But in test 2 we have changed string member two for sanity check.
891     */
892    @Test
893    public void testPayloadComputationNewItem2() {
894        AdapterItem oldItem = new AdapterItem(1, "a", "a");
895        mItems.add(oldItem);
896        mAdapter.setItems(mItems, mMockedCallback);
897
898        // Create a new item list which contain a new AdapterItem object
899        // The id of the new item is changed, and will be treated as a new item according to the
900        // rule we set in the callback. This test case is to verify the getChangePayload
901        // method still honor the standard we set up to judge new item
902        mItems.clear();
903        AdapterItem newItem = new AdapterItem(2, "a", "aa");
904        mItems.add(newItem);
905
906        // reset mocked object before calling setItems method
907        Mockito.reset(mObserver);
908        Mockito.reset(mMockedCallback);
909        mAdapter.setItems(mItems, mMockedCallback);
910
911        // Make sure only remove/ insert event will be fired under this circumstance
912        Mockito.verify(mObserver).onItemRangeRemoved(0, 1);
913        Mockito.verify(mObserver).onItemRangeInserted(0, 1);
914        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
915        Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(),
916                (AdapterItem) any());
917
918    }
919
920    /**
921     * Test payload computation process under the condition when two items are not the same
922     * based on areItemsTheSame function in DiffUtilCallback
923     *
924     * But in test 3 we have changed string member one and string member two for sanity check.
925     */
926    @Test
927    public void testPayloadComputationNewItem3() {
928        AdapterItem oldItem = new AdapterItem(1, "a", "a");
929        mItems.add(oldItem);
930        mAdapter.setItems(mItems, mMockedCallback);
931
932        // Create a new item list which contain a new AdapterItem object
933        // The id of the new item is changed, and will be treated as a new item according to the
934        // rule we set in the callback. This test case is to verify the getChangePayload
935        // method still honor the standard we set up to judge new item
936        mItems.clear();
937        AdapterItem newItem = new AdapterItem(2, "aa", "aa");
938        mItems.add(newItem);
939
940        // reset mocked object before calling setItems method
941        Mockito.reset(mObserver);
942        Mockito.reset(mMockedCallback);
943        mAdapter.setItems(mItems, mMockedCallback);
944
945        // Make sure only remove/ insert event will be fired under this circumstance
946        Mockito.verify(mObserver).onItemRangeRemoved(0, 1);
947        Mockito.verify(mObserver).onItemRangeInserted(0, 1);
948        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
949        Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(),
950                (AdapterItem) any());
951    }
952
953    /**
954     * Test payload computation process under the condition when two items have the same content
955     * based on areContentsTheSame function in DiffUtilCallback
956     */
957    @Test
958    public void testPayloadComputationSameContent() {
959        AdapterItem oldItem = new AdapterItem(1, "a", "a", "a");
960        mItems.add(oldItem);
961        mAdapter.setItems(mItems, mMockedCallback);
962
963        // Create a new item list which contain a new AdapterItem object
964        // The non-related string member of the new item is changed, but the two items are still
965        // the same as well as the item's content according to the rule we set in the callback.
966        // This test case is to verify the getChangePayload method still honor the standard
967        // we set up to determine if a new object is 1. a new item 2. has the same content as the
968        // previous one
969        mItems.clear();
970        AdapterItem newItem = new AdapterItem(1, "a", "a", "aa");
971        mItems.add(newItem);
972
973        // reset mocked object before calling setItems method
974        Mockito.reset(mObserver);
975        Mockito.reset(mMockedCallback);
976        mAdapter.setItems(mItems, mMockedCallback);
977
978        // Make sure no even will be fired up in this circumstance
979        Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt());
980        Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt());
981        Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any());
982        Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(),
983                (AdapterItem) any());
984    }
985
986
987    /**
988     * This test case is targeted at real ui testing. I.e. making sure when the change of adapter's
989     * items will trigger the rebinding of view holder with payload. That's item presenter's
990     * onBindViewHolder method with payload supporting.
991     *
992     */
993    @Test
994    public void testPresenterAndItemBridgeAdapter() {
995        // data set one
996        final List<AdapterItem> dataSetOne = new ArrayList<>();
997        AdapterItem dataSetOne0 = new AdapterItem(1, "a");
998        AdapterItem dataSetOne1 = new AdapterItem(2, "b");
999        AdapterItem dataSetOne2 = new AdapterItem(3, "c");
1000        AdapterItem dataSetOne3 = new AdapterItem(4, "d");
1001        AdapterItem dataSetOne4 = new AdapterItem(5, "3");
1002        dataSetOne.add(dataSetOne0);
1003        dataSetOne.add(dataSetOne1);
1004        dataSetOne.add(dataSetOne2);
1005        dataSetOne.add(dataSetOne3);
1006        dataSetOne.add(dataSetOne4);
1007
1008        // data set two
1009        final List<AdapterItem> dataSetTwo = new ArrayList<>();
1010        AdapterItem dataSetTwo0 = new AdapterItem(1, "aa");
1011        AdapterItem dataSetTwo1 = new AdapterItem(2, "bb");
1012        AdapterItem dataSetTwo2 = new AdapterItem(3, "cc");
1013        AdapterItem dataSetTwo3 = new AdapterItem(4, "dd");
1014        AdapterItem dataSetTwo4 = new AdapterItem(5, "ee");
1015        dataSetTwo.add(dataSetTwo0);
1016        dataSetTwo.add(dataSetTwo1);
1017        dataSetTwo.add(dataSetTwo2);
1018        dataSetTwo.add(dataSetTwo3);
1019        dataSetTwo.add(dataSetTwo4);
1020
1021        ((ArrayObjectAdapter) mRow.getAdapter()).addAll(0, dataSetOne);
1022        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
1023            @Override
1024            public void run() {
1025
1026                // obtain frame layout through context.
1027                final ViewGroup parent = new FrameLayout(mContext);
1028
1029                // create view holder and obtain the view object from view holder
1030                // add view object to our layout
1031                Presenter.ViewHolder containerVh = mListRowPresenter.onCreateViewHolder(parent);
1032                parent.addView(containerVh.view, 1000, 1000);
1033
1034                // set rows adapter and add row to that adapter
1035                mRowsAdapter = new ArrayObjectAdapter();
1036                mRowsAdapter.add(mRow);
1037
1038                // use the presenter to bind row view holder explicitly. So the itemBridgeAdapter
1039                // will be connected to the adapter inside of the listRow successfully.
1040                mListVh = (ListRowPresenter.ViewHolder) mListRowPresenter.getRowViewHolder(
1041                        containerVh);
1042                mListRowPresenter.onBindRowViewHolder(mListVh, mRow);
1043
1044                // layout the list row in recycler view
1045                runRecyclerViewLayout();
1046
1047                // reset mocked presenter
1048                Mockito.reset(mListRowPresenter);
1049                Mockito.reset(mAdapterItemPresenter);
1050
1051                // calling setItem's method to trigger the diff computation
1052                ((ArrayObjectAdapter) mRow.getAdapter()).setItems(dataSetTwo,
1053                        new DiffCallbackPayloadTesting());
1054
1055                // re-layout the recycler view to trigger getViewForPosition event
1056                runRecyclerViewLayout();
1057
1058                // verify method execution
1059                Mockito.verify(mAdapterItemPresenter, never()).onBindViewHolder(
1060                        (RowPresenter.ViewHolder) any(), (Object) any());
1061                Mockito.verify(mAdapterItemPresenter, atLeast(5)).onBindViewHolder(
1062                        (RowPresenter.ViewHolder) any(), (Object) any(), (List<Object>) any());
1063            }
1064        });
1065    }
1066
1067    /**
1068     * Helper function to layout recycler view
1069     * So the recycler view will execute the getView() method then the onBindViewHolder() method
1070     * from presenter will be executed
1071     */
1072    private void runRecyclerViewLayout() {
1073        mListVh.view.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
1074                View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY));
1075        mListVh.view.layout(0, 0, 1000, 1000);
1076    }
1077
1078    /**
1079     * Helper function to compare two bundles through iterating the fields.
1080     *
1081     * @param bundle1 bundle 1
1082     * @param bundle2 bundle 2
1083     */
1084    private void compareTwoBundles(Bundle bundle1, Bundle bundle2) {
1085        assertEquals(bundle1.getInt(ID), bundle2.getInt(ID));
1086        assertEquals(bundle1.getString(STRING_MEMBER_ONE), bundle2.getString(
1087                STRING_MEMBER_ONE));
1088        assertEquals(bundle1.getString(STRING_MEMBER_TWO), bundle2.getString(
1089                STRING_MEMBER_TWO));
1090        assertEquals(bundle1.getString(NOT_RELATED_STRING_MEMBER),
1091                bundle2.getString(NOT_RELATED_STRING_MEMBER));
1092    }
1093
1094    /**
1095     * Helper function to test the content in adapter
1096     */
1097    private static void assertAdapterContent(ObjectAdapter adapter, Object[] data) {
1098        assertEquals(adapter.size(), data.length);
1099        for (int i = 0; i < adapter.size(); i++) {
1100            assertEquals(adapter.get(i), data[i]);
1101        }
1102    }
1103}
1104