| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.leanback.widget; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.atLeast; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.times; |
| |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.SmallTest; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| |
| import androidx.annotation.Nullable; |
| import androidx.leanback.R; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| import org.mockito.Mockito; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| @SmallTest |
| @RunWith(JUnit4.class) |
| public class ObjectAdapterTest { |
| |
| private static final String ID = "id"; |
| private static final String STRING_MEMBER_ONE = "stringMemberOne"; |
| private static final String STRING_MEMBER_TWO = "stringMemberTwo"; |
| private static final String NOT_RELATED_STRING_MEMBER = "notRelatedStringMember"; |
| |
| protected ItemBridgeAdapter mBridgeAdapter; |
| protected ArrayObjectAdapter mAdapter; |
| |
| private ArrayList mItems; |
| private DiffCallback<AdapterItem> mMockedCallback; |
| private DiffCallback<AdapterItem> mCallbackWithoutPayload; |
| private RecyclerView.AdapterDataObserver mObserver; |
| |
| private Context mContext; |
| private ListRowPresenter mListRowPresenter; |
| private ListRowPresenter.ViewHolder mListVh; |
| private ArrayObjectAdapter mRowsAdapter; |
| private AdapterItemPresenter mAdapterItemPresenter; |
| |
| private ListRow mRow; |
| |
| /** |
| * This type is used to test setItems() API. |
| */ |
| private static class AdapterItem { |
| private int mId; |
| private String mStringMemberOne; |
| |
| // mStringMemberTwo is only used to test if correct payload can be generated. |
| private String mStringMemberTwo; |
| |
| // not related string will not impact the result of our equals function. |
| // Used to verify if payload computing process still honor the rule set by |
| // areContentsTheSame() method |
| private String mNotRelatedStringMember; |
| |
| AdapterItem(int id, String stringMemberOne) { |
| mId = id; |
| mStringMemberOne = stringMemberOne; |
| mStringMemberTwo = ""; |
| mNotRelatedStringMember = ""; |
| } |
| |
| AdapterItem(int id, String stringMemberOne, String stringMemberTwo) { |
| mId = id; |
| mStringMemberOne = stringMemberOne; |
| mStringMemberTwo = stringMemberTwo; |
| mNotRelatedStringMember = ""; |
| } |
| |
| AdapterItem(int id, String stringMemberOne, String stringMemberTwo, |
| String notRelatedStringMember) { |
| mId = id; |
| mStringMemberOne = stringMemberOne; |
| mStringMemberTwo = stringMemberTwo; |
| mNotRelatedStringMember = notRelatedStringMember; |
| } |
| |
| public int getId() { |
| return mId; |
| } |
| |
| public String getStringMemberOne() { |
| return mStringMemberOne; |
| } |
| |
| public String getStringMemberTwo() { |
| return mStringMemberTwo; |
| } |
| |
| public String getNotRelatedStringMember() { |
| return mNotRelatedStringMember; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| AdapterItem that = (AdapterItem) o; |
| |
| if (mId != that.mId) return false; |
| if (mStringMemberOne != null ? !mStringMemberOne.equals(that.mStringMemberOne) |
| : that.mStringMemberOne != null) { |
| return false; |
| } |
| return mStringMemberTwo != null ? mStringMemberTwo.equals(that.mStringMemberTwo) |
| : that.mStringMemberTwo == null; |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = mId; |
| result = 31 * result + (mStringMemberOne != null ? mStringMemberOne.hashCode() : 0); |
| result = 31 * result + (mStringMemberTwo != null ? mStringMemberTwo.hashCode() : 0); |
| return result; |
| } |
| } |
| |
| /** |
| * Extend from DiffCallback extended class to define the rule to compare if two items are the |
| * same/ have the same content and how to calculate the payload. |
| * |
| * The payload will only be calculated when the two items are the same but with different |
| * contents. So we make this class as a public class which can be mocked by mockito to verify |
| * if the calculation process satisfies our requirement. |
| */ |
| public static class DiffCallbackPayloadTesting extends DiffCallback<AdapterItem> { |
| // Using item's mId as the standard to judge if two items is the same |
| @Override |
| public boolean areItemsTheSame(AdapterItem oldItem, AdapterItem newItem) { |
| return oldItem.getId() == newItem.getId(); |
| } |
| |
| // Using equals method to judge if two items have the same content. |
| @Override |
| public boolean areContentsTheSame(AdapterItem oldItem, AdapterItem newItem) { |
| return oldItem.equals(newItem); |
| } |
| |
| @Nullable |
| @Override |
| public Object getChangePayload(AdapterItem oldItem, |
| AdapterItem newItem) { |
| Bundle diff = new Bundle(); |
| if (oldItem.getId() != newItem.getId()) { |
| diff.putInt(ID, newItem.getId()); |
| } |
| |
| if (!oldItem.getStringMemberOne().equals(newItem.getStringMemberOne())) { |
| diff.putString(STRING_MEMBER_ONE, newItem.getStringMemberOne()); |
| } |
| |
| if (!oldItem.getStringMemberTwo().equals(newItem.getStringMemberTwo())) { |
| diff.putString(STRING_MEMBER_TWO, newItem.getStringMemberTwo()); |
| } |
| |
| if (!oldItem.getNotRelatedStringMember().equals(newItem.getNotRelatedStringMember())) { |
| diff.putString(NOT_RELATED_STRING_MEMBER, newItem.getNotRelatedStringMember()); |
| } |
| |
| if (diff.size() == 0) { |
| return null; |
| } |
| return diff; |
| } |
| } |
| |
| /** |
| * The presenter designed for adapter item. |
| * |
| * The reason to set this class as a public class is for Mockito to mock it. So we can observe |
| * method's dispatching easily |
| */ |
| public static class AdapterItemPresenter extends Presenter { |
| int mWidth; |
| int mHeight; |
| |
| AdapterItemPresenter() { |
| this(100, 100); |
| } |
| |
| AdapterItemPresenter(int width, int height) { |
| mWidth = width; |
| mHeight = height; |
| } |
| |
| @Override |
| public ViewHolder onCreateViewHolder(ViewGroup parent) { |
| View view = new View(parent.getContext()); |
| view.setFocusable(true); |
| view.setId(R.id.lb_action_button); |
| view.setLayoutParams(new ViewGroup.LayoutParams(mWidth, mHeight)); |
| return new Presenter.ViewHolder(view); |
| } |
| |
| @Override |
| public void onBindViewHolder(ViewHolder viewHolder, Object item) { |
| // no - op |
| } |
| |
| @Override |
| public void onUnbindViewHolder(ViewHolder viewHolder) { |
| // no - op |
| } |
| |
| @Override |
| public void onBindViewHolder(ViewHolder viewHolder, Object item, |
| List<Object> payloads) { |
| // no - op |
| } |
| } |
| |
| |
| /** |
| * Initialize test-related members. |
| */ |
| @Before |
| public void setup() { |
| mAdapter = new ArrayObjectAdapter(); |
| mBridgeAdapter = new ItemBridgeAdapter(mAdapter); |
| mItems = new ArrayList(); |
| mMockedCallback = Mockito.spy(DiffCallbackPayloadTesting.class); |
| |
| // the diff callback without calculating the payload |
| mCallbackWithoutPayload = new DiffCallback<AdapterItem>() { |
| |
| // Using item's mId as the standard to judge if two items is the same |
| @Override |
| public boolean areItemsTheSame(AdapterItem oldItem, AdapterItem newItem) { |
| return oldItem.getId() == newItem.getId(); |
| } |
| |
| // Using equals method to judge if two items have the same content. |
| @Override |
| public boolean areContentsTheSame(AdapterItem oldItem, AdapterItem newItem) { |
| return oldItem.equals(newItem); |
| } |
| }; |
| |
| // Spy the RecyclerView.AdapterObserver |
| mObserver = Mockito.spy(RecyclerView.AdapterDataObserver.class); |
| |
| // register observer so we can observe the events |
| mBridgeAdapter.registerAdapterDataObserver(mObserver); |
| |
| // obtain context through instrumentation registry |
| mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); |
| |
| // |
| ListRowPresenter listRowPresenter = new ListRowPresenter(); |
| mListRowPresenter = Mockito.spy(listRowPresenter); |
| |
| // mock item presenter |
| AdapterItemPresenter adapterItemPresenter = new AdapterItemPresenter(); |
| mAdapterItemPresenter = Mockito.spy(adapterItemPresenter); |
| mRow = new ListRow(new ArrayObjectAdapter(mAdapterItemPresenter)); |
| } |
| |
| /** |
| * The following test case is mainly focused on the basic functionality provided by |
| * Object Adapter. |
| * |
| * The key purpose for this test is to make sure when adapter send out a signal through |
| * notify function, it will finally be intercepted by recycler view's observer |
| */ |
| @Test |
| public void testBasicFunctionality() { |
| mItems.add("a"); |
| mItems.add("b"); |
| mItems.add("c"); |
| mAdapter.addAll(0, mItems); |
| |
| // size |
| assertEquals(mAdapter.size(), 3); |
| |
| // get |
| assertEquals(mAdapter.get(0), "a"); |
| assertEquals(mAdapter.get(1), "b"); |
| assertEquals(mAdapter.get(2), "c"); |
| |
| // indexOf |
| assertEquals(mAdapter.indexOf("a"), 0); |
| assertEquals(mAdapter.indexOf("b"), 1); |
| assertEquals(mAdapter.indexOf("c"), 2); |
| |
| // insert |
| mAdapter.add(1, "a1"); |
| Mockito.verify(mObserver).onItemRangeInserted(1, 1); |
| assertAdapterContent(mAdapter, new Object[]{"a", "a1", "b", "c"}); |
| Mockito.reset(mObserver); |
| |
| // insert multiple |
| ArrayList newItems1 = new ArrayList(); |
| newItems1.add("a2"); |
| newItems1.add("a3"); |
| mAdapter.addAll(1, newItems1); |
| Mockito.verify(mObserver).onItemRangeInserted(1, 2); |
| assertAdapterContent(mAdapter, new Object[]{"a", "a2", "a3", "a1", "b", "c"}); |
| Mockito.reset(mObserver); |
| |
| // update |
| mAdapter.notifyArrayItemRangeChanged(2, 3); |
| Mockito.verify(mObserver).onItemRangeChanged(2, 3); |
| assertAdapterContent(mAdapter, new Object[]{"a", "a2", "a3", "a1", "b", "c"}); |
| Mockito.reset(mObserver); |
| |
| // remove |
| mAdapter.removeItems(1, 4); |
| Mockito.verify(mObserver).onItemRangeRemoved(1, 4); |
| assertAdapterContent(mAdapter, new Object[]{"a", "c"}); |
| Mockito.reset(mObserver); |
| |
| // move |
| mAdapter.move(0, 1); |
| Mockito.verify(mObserver).onItemRangeMoved(0, 1, 1); |
| assertAdapterContent(mAdapter, new Object[]{"c", "a"}); |
| Mockito.reset(mObserver); |
| |
| // replace |
| mAdapter.replace(0, "a"); |
| Mockito.verify(mObserver).onItemRangeChanged(0, 1); |
| assertAdapterContent(mAdapter, new Object[]{"a", "a"}); |
| Mockito.reset(mObserver); |
| mAdapter.replace(1, "b"); |
| Mockito.verify(mObserver).onItemRangeChanged(1, 1); |
| assertAdapterContent(mAdapter, new Object[]{"a", "b"}); |
| Mockito.reset(mObserver); |
| |
| // remove multiple |
| mItems.clear(); |
| mItems.add("a"); |
| mItems.add("b"); |
| mAdapter.addAll(0, mItems); |
| mAdapter.removeItems(0, 2); |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 2); |
| assertAdapterContent(mAdapter, new Object[]{"a", "b"}); |
| Mockito.reset(mObserver); |
| |
| // clear |
| mAdapter.clear(); |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 2); |
| assertAdapterContent(mAdapter, new Object[]{}); |
| Mockito.reset(mObserver); |
| |
| // isImmediateNotifySupported |
| assertTrue(mAdapter.isImmediateNotifySupported()); |
| } |
| |
| |
| @Test |
| public void testSetItemsNoDiffCallback() { |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, null); |
| Mockito.verify(mObserver, times(1)).onChanged(); |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| |
| mItems.add(new AdapterItem(4, "a")); |
| mItems.add(new AdapterItem(5, "b")); |
| mItems.add(new AdapterItem(6, "c")); |
| mAdapter.setItems(mItems, null); |
| Mockito.verify(mObserver, times(2)).onChanged(); |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| } |
| |
| /** |
| * The following test cases are mainly focused on the basic functionality provided by setItems |
| * function |
| * |
| * It can be deemed as an extension to the previous test, and won't consider payload in this |
| * test case. |
| * |
| * Test0 will treat all items as the same item with same content. |
| */ |
| @Test |
| public void testSetItemsMethod0() { |
| mItems.add("a"); |
| mItems.add("b"); |
| mItems.add("c"); |
| |
| DiffCallback<String> callback = new DiffCallback<String>() { |
| |
| // Always treat two items are the same. |
| @Override |
| public boolean areItemsTheSame(String oldItem, String newItem) { |
| return true; |
| } |
| |
| // Always treat two items have the same content. |
| @Override |
| public boolean areContentsTheSame(String oldItem, String newItem) { |
| return true; |
| } |
| }; |
| |
| mAdapter.setItems(mItems, callback); |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeInserted(0, 3); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add("a"); |
| mItems.add("b"); |
| mItems.add("c"); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, callback); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| assertAdapterContent(mAdapter, new Object[]{"a", "b", "c"}); |
| } |
| |
| /** |
| * Test1 will treat all items as the same item with same content. |
| */ |
| @Test |
| public void testSetItemsMethod1() { |
| mItems.add("a"); |
| mItems.add("b"); |
| mItems.add("c"); |
| |
| DiffCallback<String> callback = new DiffCallback<String>() { |
| |
| // Always treat two items are the different. |
| @Override |
| public boolean areItemsTheSame(String oldItem, String newItem) { |
| return false; |
| } |
| |
| // Always treat two items have the different content. |
| @Override |
| public boolean areContentsTheSame(String oldItem, String newItem) { |
| return false; |
| } |
| }; |
| |
| mAdapter.setItems(mItems, callback); |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeInserted(0, 3); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add("a"); |
| mItems.add("b"); |
| mItems.add("c"); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, callback); |
| |
| // No change or move event should be fired under current callback. |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 3); |
| Mockito.verify(mObserver).onItemRangeInserted(0, 3); |
| assertAdapterContent(mAdapter, new Object[]{"a", "b", "c"}); |
| } |
| |
| /** |
| * Test2 will trigger notifyItemRangeChanged event |
| */ |
| @Test |
| public void testSetItemsMethod2() { |
| // initial item list |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "c")); |
| mItems.add(new AdapterItem(3, "b")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeChanged(1, 2, null); |
| } |
| |
| |
| /** |
| * Test3 will trigger notifyItemMoved event |
| */ |
| @Test |
| public void testSetItemsMethod3() { |
| // initial item list |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(3, "c")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeMoved(1, 0, 1); |
| } |
| |
| /** |
| * Test4 will trigger notifyItemRangeRemoved event |
| */ |
| @Test |
| public void testSetItemsMethod4() { |
| // initial item list |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 1); |
| } |
| |
| /** |
| * Test5 will trigger notifyItemRangeInserted event |
| */ |
| @Test |
| public void testSetItemsMethod5() { |
| // initial item list |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mItems.add(new AdapterItem(4, "d")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeInserted(3, 1); |
| } |
| |
| |
| /** |
| * Test6 will trigger notifyItemRangeInserted event and notifyItemRangeRemoved event |
| * simultaneously |
| */ |
| @Test |
| public void testSetItemsMethod6() { |
| // initial item list |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(2, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 1); |
| Mockito.verify(mObserver).onItemRangeInserted(0, 1); |
| } |
| |
| /** |
| * Test7 will trigger notifyItemRangeMoved and notifyItemRangeChanged event simultaneously |
| */ |
| @Test |
| public void testItemsMethod7() { |
| // initial item list |
| mItems.add(new AdapterItem(1, "a")); |
| mItems.add(new AdapterItem(2, "b")); |
| mItems.add(new AdapterItem(3, "c")); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(1, "aa")); |
| mItems.add(new AdapterItem(3, "c")); |
| mItems.add(new AdapterItem(2, "b")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeChanged(0, 1, null); |
| Mockito.verify(mObserver).onItemRangeMoved(2, 1, 1); |
| } |
| |
| /** |
| * Test8 will trigger multiple items insertion event |
| */ |
| @Test |
| public void testSetItemsMethod8() { |
| |
| // initial item list |
| mAdapter.clear(); |
| mItems.add(new AdapterItem(0, "a")); |
| mItems.add(new AdapterItem(1, "b")); |
| mAdapter.clear(); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // Clear previous items and set a new list of items. |
| mItems.clear(); |
| mItems.add(new AdapterItem(0, "a")); |
| mItems.add(new AdapterItem(1, "b")); |
| mItems.add(new AdapterItem(2, "c")); |
| mItems.add(new AdapterItem(3, "d")); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| mAdapter.setItems(mItems, mCallbackWithoutPayload); |
| |
| // verify method dispatching |
| Mockito.verify(mObserver).onItemRangeInserted(2, 2); |
| Mockito.reset(mObserver); |
| } |
| |
| |
| /** |
| * The following test cases are mainly focused on testing setItems method when we need to |
| * calculate payload |
| * |
| * The payload should only be calculated when two items are same but with different contents. |
| * I.e. the calculate payload method should only be executed when the previous condition is |
| * satisfied. In this test case we use a mocked callback object to verify it and compare the |
| * calculated payload with our expected payload. |
| * |
| * Test 0 will calculate the difference on string member one. |
| */ |
| @Test |
| public void testPayloadCalculation0() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // test if payload is computed correctly by changing string member 1 |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(1, "aa", "a"); |
| mItems.add(newItem); |
| |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create expected payload manually for verification |
| Bundle expectedPayload0 = new Bundle(); |
| expectedPayload0.putString(STRING_MEMBER_ONE, newItem.getStringMemberOne()); |
| |
| // make sure no other event will be triggered in current scenario |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| |
| // Check if getChangePayload is executed as we expected |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), eq(null)); |
| Mockito.verify(mMockedCallback).getChangePayload(oldItem, |
| newItem); |
| |
| // compare the two bundles by iterating each member |
| Bundle calculatedBundle0 = (Bundle) mMockedCallback.getChangePayload( |
| oldItem, newItem); |
| compareTwoBundles(calculatedBundle0, expectedPayload0); |
| |
| } |
| |
| /** |
| * Test 1 will calculate the difference on string member two. |
| */ |
| @Test |
| public void testPayloadComputation1() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // test if payload is computed correctly by changing string member 2 |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(1, "a", "aa"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create expected payload manually for verification |
| Bundle expectedPayload0 = new Bundle(); |
| expectedPayload0.putString(STRING_MEMBER_TWO, newItem.getStringMemberTwo()); |
| |
| // make sure no other event will be triggered in current scenario |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| |
| // Check if getChangePayload is executed as we expected |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), eq(null)); |
| Mockito.verify(mMockedCallback).getChangePayload(oldItem, |
| newItem); |
| |
| // compare the two bundles by iterating each member |
| Bundle calculatedBundle0 = (Bundle) mMockedCallback.getChangePayload( |
| oldItem, newItem); |
| compareTwoBundles(calculatedBundle0, expectedPayload0); |
| |
| } |
| |
| /** |
| * Test 1 will calculate the difference on string member one and string member two. |
| */ |
| @Test |
| public void testPayloadComputation2() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // test if payload is computed correctly by changing string member 1 and string member 2 |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(1, "aa", "aa"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create expected payload manually for verification |
| Bundle expectedPayload0 = new Bundle(); |
| expectedPayload0.putString(STRING_MEMBER_ONE, newItem.getStringMemberOne()); |
| expectedPayload0.putString(STRING_MEMBER_TWO, newItem.getStringMemberTwo()); |
| |
| // make sure no other event will be triggered in current scenario |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeMoved(anyInt(), anyInt(), anyInt()); |
| |
| // Check if getChangePayload is executed as we expected |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), eq(null)); |
| Mockito.verify(mMockedCallback).getChangePayload(oldItem, |
| newItem); |
| |
| // compare the two bundles by iterating each member |
| Bundle calculatedBundle0 = (Bundle) mMockedCallback.getChangePayload( |
| oldItem, newItem); |
| compareTwoBundles(calculatedBundle0, expectedPayload0); |
| |
| } |
| |
| /** |
| * Test payload computation process under the condition when two items are not the same |
| * based on areItemsTheSame function in DiffUtilCallback |
| */ |
| @Test |
| public void testPayloadComputationNewItem0() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // The id of the new item is changed, and will be treated as a new item according to the |
| // rule we set in the callback. This test case is to verify the getChangePayload |
| // method still honor the standard we set up to judge new item |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(2, "a", "a"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Make sure only remove/ insert event will be fired under this circumstance |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 1); |
| Mockito.verify(mObserver).onItemRangeInserted(0, 1); |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(), |
| (AdapterItem) any()); |
| |
| } |
| |
| /** |
| * Test payload computation process under the condition when two items are not the same |
| * based on areItemsTheSame function in DiffUtilCallback |
| * |
| * But in test 1 we have changed string member one for sanity check. |
| */ |
| @Test |
| public void testPayloadComputationNewItem1() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // The id of the new item is changed, and will be treated as a new item according to the |
| // rule we set in the callback. This test case is to verify the getChangePayload |
| // method still honor the standard we set up to judge new item |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(2, "aa", "a"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Make sure only remove/ insert event will be fired under this circumstance |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 1); |
| Mockito.verify(mObserver).onItemRangeInserted(0, 1); |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(), |
| (AdapterItem) any()); |
| |
| } |
| |
| /** |
| * Test payload computation process under the condition when two items are not the same |
| * based on areItemsTheSame function in DiffUtilCallback |
| * |
| * But in test 2 we have changed string member two for sanity check. |
| */ |
| @Test |
| public void testPayloadComputationNewItem2() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // The id of the new item is changed, and will be treated as a new item according to the |
| // rule we set in the callback. This test case is to verify the getChangePayload |
| // method still honor the standard we set up to judge new item |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(2, "a", "aa"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Make sure only remove/ insert event will be fired under this circumstance |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 1); |
| Mockito.verify(mObserver).onItemRangeInserted(0, 1); |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(), |
| (AdapterItem) any()); |
| |
| } |
| |
| /** |
| * Test payload computation process under the condition when two items are not the same |
| * based on areItemsTheSame function in DiffUtilCallback |
| * |
| * But in test 3 we have changed string member one and string member two for sanity check. |
| */ |
| @Test |
| public void testPayloadComputationNewItem3() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // The id of the new item is changed, and will be treated as a new item according to the |
| // rule we set in the callback. This test case is to verify the getChangePayload |
| // method still honor the standard we set up to judge new item |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(2, "aa", "aa"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Make sure only remove/ insert event will be fired under this circumstance |
| Mockito.verify(mObserver).onItemRangeRemoved(0, 1); |
| Mockito.verify(mObserver).onItemRangeInserted(0, 1); |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(), |
| (AdapterItem) any()); |
| } |
| |
| /** |
| * Test payload computation process under the condition when two items have the same content |
| * based on areContentsTheSame function in DiffUtilCallback |
| */ |
| @Test |
| public void testPayloadComputationSameContent() { |
| AdapterItem oldItem = new AdapterItem(1, "a", "a", "a"); |
| mItems.add(oldItem); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Create a new item list which contain a new AdapterItem object |
| // The non-related string member of the new item is changed, but the two items are still |
| // the same as well as the item's content according to the rule we set in the callback. |
| // This test case is to verify the getChangePayload method still honor the standard |
| // we set up to determine if a new object is 1. a new item 2. has the same content as the |
| // previous one |
| mItems.clear(); |
| AdapterItem newItem = new AdapterItem(1, "a", "a", "aa"); |
| mItems.add(newItem); |
| |
| // reset mocked object before calling setItems method |
| Mockito.reset(mObserver); |
| Mockito.reset(mMockedCallback); |
| mAdapter.setItems(mItems, mMockedCallback); |
| |
| // Make sure no even will be fired up in this circumstance |
| Mockito.verify(mObserver, never()).onItemRangeRemoved(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeInserted(anyInt(), anyInt()); |
| Mockito.verify(mObserver, never()).onItemRangeChanged(anyInt(), anyInt(), any()); |
| Mockito.verify(mMockedCallback, never()).getChangePayload((AdapterItem) any(), |
| (AdapterItem) any()); |
| } |
| |
| |
| /** |
| * This test case is targeted at real ui testing. I.e. making sure when the change of adapter's |
| * items will trigger the rebinding of view holder with payload. That's item presenter's |
| * onBindViewHolder method with payload supporting. |
| * |
| */ |
| @Test |
| public void testPresenterAndItemBridgeAdapter() { |
| // data set one |
| final List<AdapterItem> dataSetOne = new ArrayList<>(); |
| AdapterItem dataSetOne0 = new AdapterItem(1, "a"); |
| AdapterItem dataSetOne1 = new AdapterItem(2, "b"); |
| AdapterItem dataSetOne2 = new AdapterItem(3, "c"); |
| AdapterItem dataSetOne3 = new AdapterItem(4, "d"); |
| AdapterItem dataSetOne4 = new AdapterItem(5, "3"); |
| dataSetOne.add(dataSetOne0); |
| dataSetOne.add(dataSetOne1); |
| dataSetOne.add(dataSetOne2); |
| dataSetOne.add(dataSetOne3); |
| dataSetOne.add(dataSetOne4); |
| |
| // data set two |
| final List<AdapterItem> dataSetTwo = new ArrayList<>(); |
| AdapterItem dataSetTwo0 = new AdapterItem(1, "aa"); |
| AdapterItem dataSetTwo1 = new AdapterItem(2, "bb"); |
| AdapterItem dataSetTwo2 = new AdapterItem(3, "cc"); |
| AdapterItem dataSetTwo3 = new AdapterItem(4, "dd"); |
| AdapterItem dataSetTwo4 = new AdapterItem(5, "ee"); |
| dataSetTwo.add(dataSetTwo0); |
| dataSetTwo.add(dataSetTwo1); |
| dataSetTwo.add(dataSetTwo2); |
| dataSetTwo.add(dataSetTwo3); |
| dataSetTwo.add(dataSetTwo4); |
| |
| ((ArrayObjectAdapter) mRow.getAdapter()).addAll(0, dataSetOne); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { |
| @Override |
| public void run() { |
| |
| // obtain frame layout through context. |
| final ViewGroup parent = new FrameLayout(mContext); |
| |
| // create view holder and obtain the view object from view holder |
| // add view object to our layout |
| Presenter.ViewHolder containerVh = mListRowPresenter.onCreateViewHolder(parent); |
| parent.addView(containerVh.view, 1000, 1000); |
| |
| // set rows adapter and add row to that adapter |
| mRowsAdapter = new ArrayObjectAdapter(); |
| mRowsAdapter.add(mRow); |
| |
| // use the presenter to bind row view holder explicitly. So the itemBridgeAdapter |
| // will be connected to the adapter inside of the listRow successfully. |
| mListVh = (ListRowPresenter.ViewHolder) mListRowPresenter.getRowViewHolder( |
| containerVh); |
| mListRowPresenter.onBindRowViewHolder(mListVh, mRow); |
| |
| // layout the list row in recycler view |
| runRecyclerViewLayout(); |
| |
| // reset mocked presenter |
| Mockito.reset(mListRowPresenter); |
| Mockito.reset(mAdapterItemPresenter); |
| |
| // calling setItem's method to trigger the diff computation |
| ((ArrayObjectAdapter) mRow.getAdapter()).setItems(dataSetTwo, |
| new DiffCallbackPayloadTesting()); |
| |
| // re-layout the recycler view to trigger getViewForPosition event |
| runRecyclerViewLayout(); |
| |
| // verify method execution |
| Mockito.verify(mAdapterItemPresenter, never()).onBindViewHolder( |
| (RowPresenter.ViewHolder) any(), (Object) any()); |
| Mockito.verify(mAdapterItemPresenter, atLeast(5)).onBindViewHolder( |
| (RowPresenter.ViewHolder) any(), (Object) any(), (List<Object>) any()); |
| } |
| }); |
| } |
| |
| /** |
| * Helper function to layout recycler view |
| * So the recycler view will execute the getView() method then the onBindViewHolder() method |
| * from presenter will be executed |
| */ |
| private void runRecyclerViewLayout() { |
| mListVh.view.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), |
| View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); |
| mListVh.view.layout(0, 0, 1000, 1000); |
| } |
| |
| /** |
| * Helper function to compare two bundles through iterating the fields. |
| * |
| * @param bundle1 bundle 1 |
| * @param bundle2 bundle 2 |
| */ |
| private void compareTwoBundles(Bundle bundle1, Bundle bundle2) { |
| assertEquals(bundle1.getInt(ID), bundle2.getInt(ID)); |
| assertEquals(bundle1.getString(STRING_MEMBER_ONE), bundle2.getString( |
| STRING_MEMBER_ONE)); |
| assertEquals(bundle1.getString(STRING_MEMBER_TWO), bundle2.getString( |
| STRING_MEMBER_TWO)); |
| assertEquals(bundle1.getString(NOT_RELATED_STRING_MEMBER), |
| bundle2.getString(NOT_RELATED_STRING_MEMBER)); |
| } |
| |
| /** |
| * Helper function to test the content in adapter |
| */ |
| private static void assertAdapterContent(ObjectAdapter adapter, Object[] data) { |
| assertEquals(adapter.size(), data.length); |
| for (int i = 0; i < adapter.size(); i++) { |
| assertEquals(adapter.get(i), data[i]); |
| } |
| } |
| } |