From ac3df6bc5eb71a5099f7a6c6fc04459b74e1101e Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 17 Jul 2020 09:04:16 -0700 Subject: [PATCH] Add tests for adapters in collections (#12649) --- .../CollectionCreationTabListAdapter.kt | 67 +++----- .../collections/SaveCollectionListAdapter.kt | 11 +- .../mozilla/fenix/collections/TabDiffUtil.kt | 63 ++++++++ .../CollectionCreationTabListAdapterTest.kt | 89 +++++++++++ .../SaveCollectionListAdapterTest.kt | 80 ++++++++++ .../fenix/collections/TabDiffUtilTest.kt | 151 ++++++++++++++++++ 6 files changed, 407 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt create mode 100644 app/src/test/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapterTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/collections/SaveCollectionListAdapterTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/collections/TabDiffUtilTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt index a936608b2..e74cc340e 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt @@ -11,22 +11,24 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.collection_tab_list_row.view.* +import kotlinx.android.synthetic.main.collection_tab_list_row.* import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.home.Tab +import org.mozilla.fenix.utils.view.ViewHolder class CollectionCreationTabListAdapter( private val interactor: CollectionCreationInteractor ) : RecyclerView.Adapter() { + private var tabs: List = listOf() private var selectedTabs: MutableSet = mutableSetOf() private var hideCheckboxes = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder { - val view = - LayoutInflater.from(parent.context).inflate(TabViewHolder.LAYOUT_ID, parent, false) + val view = LayoutInflater.from(parent.context) + .inflate(TabViewHolder.LAYOUT_ID, parent, false) return TabViewHolder(view) } @@ -39,11 +41,11 @@ class CollectionCreationTabListAdapter( is CheckChanged -> { val checkChanged = payloads[0] as CheckChanged if (checkChanged.shouldBeChecked) { - holder.itemView.tab_selected_checkbox.isChecked = true + holder.tab_selected_checkbox.isChecked = true } else if (checkChanged.shouldBeUnchecked) { - holder.itemView.tab_selected_checkbox.isChecked = false + holder.tab_selected_checkbox.isChecked = false } - holder.itemView.tab_selected_checkbox.isGone = checkChanged.shouldHideCheckBox + holder.tab_selected_checkbox.isGone = checkChanged.shouldHideCheckBox } } } @@ -52,7 +54,7 @@ class CollectionCreationTabListAdapter( override fun onBindViewHolder(holder: TabViewHolder, position: Int) { val tab = tabs[position] val isSelected = selectedTabs.contains(tab) - holder.itemView.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked -> + holder.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { selectedTabs.add(tab) interactor.addTabToSelection(tab) @@ -86,57 +88,24 @@ class CollectionCreationTabListAdapter( } } -private class TabDiffUtil( - val old: List, - val new: List, - val oldSelected: Set, - val newSelected: Set, - val oldHideCheckboxes: Boolean, - val newHideCheckboxes: Boolean -) : DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - old[oldItemPosition].sessionId == new[newItemPosition].sessionId - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val isSameTab = old[oldItemPosition].sessionId == new[newItemPosition].sessionId - val sameSelectedState = oldSelected.contains(old[oldItemPosition]) == newSelected.contains(new[newItemPosition]) - val isSameHideCheckboxes = oldHideCheckboxes == newHideCheckboxes - return isSameTab && sameSelectedState && isSameHideCheckboxes - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val shouldBeChecked = newSelected.contains(new[newItemPosition]) && !oldSelected.contains(old[oldItemPosition]) - val shouldBeUnchecked = - !newSelected.contains(new[newItemPosition]) && oldSelected.contains(old[oldItemPosition]) - return CheckChanged(shouldBeChecked, shouldBeUnchecked, newHideCheckboxes) - } - - override fun getOldListSize(): Int = old.size - override fun getNewListSize(): Int = new.size -} - -data class CheckChanged(val shouldBeChecked: Boolean, val shouldBeUnchecked: Boolean, val shouldHideCheckBox: Boolean) - -class TabViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - private val checkbox = view.tab_selected_checkbox!! +class TabViewHolder(view: View) : ViewHolder(view) { init { - view.collection_item_tab.setOnClickListener { - checkbox.isChecked = !checkbox.isChecked + collection_item_tab.setOnClickListener { + tab_selected_checkbox.isChecked = !tab_selected_checkbox.isChecked } } fun bind(tab: Tab, isSelected: Boolean, shouldHideCheckBox: Boolean) { - itemView.hostname.text = tab.hostname - itemView.tab_title.text = tab.title - checkbox.isInvisible = shouldHideCheckBox + hostname.text = tab.hostname + tab_title.text = tab.title + tab_selected_checkbox.isInvisible = shouldHideCheckBox itemView.isClickable = !shouldHideCheckBox - if (checkbox.isChecked != isSelected) { - checkbox.isChecked = isSelected + if (tab_selected_checkbox.isChecked != isSelected) { + tab_selected_checkbox.isChecked = isSelected } - itemView.context.components.core.icons.loadIntoView(itemView.favicon_image, tab.url) + itemView.context.components.core.icons.loadIntoView(favicon_image, tab.url) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt index fa35f442c..0ee663b53 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt @@ -10,12 +10,13 @@ import android.view.ViewGroup import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat.SRC_IN import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.collections_list_item.view.* +import kotlinx.android.synthetic.main.collections_list_item.* import mozilla.components.feature.tab.collections.TabCollection import org.mozilla.fenix.R import org.mozilla.fenix.components.description import org.mozilla.fenix.ext.getIconColor import org.mozilla.fenix.home.Tab +import org.mozilla.fenix.utils.view.ViewHolder class SaveCollectionListAdapter( private val interactor: CollectionCreationInteractor @@ -48,12 +49,12 @@ class SaveCollectionListAdapter( } } -class CollectionViewHolder(view: View) : RecyclerView.ViewHolder(view) { +class CollectionViewHolder(view: View) : ViewHolder(view) { fun bind(collection: TabCollection) { - itemView.collection_item.text = collection.title - itemView.collection_description.text = collection.description(itemView.context) - itemView.collection_icon.colorFilter = + collection_item.text = collection.title + collection_description.text = collection.description(itemView.context) + collection_icon.colorFilter = createBlendModeColorFilterCompat(collection.getIconColor(itemView.context), SRC_IN) } diff --git a/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt b/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt new file mode 100644 index 000000000..a555dd8f0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.collections + +import androidx.recyclerview.widget.DiffUtil +import org.mozilla.fenix.home.Tab + +/** + * Diff callback for comparing tab lists with selected state. + */ +internal class TabDiffUtil( + private val old: List, + private val new: List, + private val oldSelected: Set, + private val newSelected: Set, + private val oldHideCheckboxes: Boolean, + private val newHideCheckboxes: Boolean +) : DiffUtil.Callback() { + + /** + * Checks if the tabs in the given positions refer to the same tab (based on ID). + */ + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + old[oldItemPosition].sessionId == new[newItemPosition].sessionId + + /** + * Checks if the combination of tab ID, selection, and checkbox visibility is the same. + */ + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val isSameTab = old[oldItemPosition].sessionId == new[newItemPosition].sessionId + val sameSelectedState = oldItemSelected(oldItemPosition) == newItemSelected(newItemPosition) + val isSameHideCheckboxes = oldHideCheckboxes == newHideCheckboxes + return isSameTab && sameSelectedState && isSameHideCheckboxes + } + + /** + * Returns a change payload indication if the item is now/no longer selected. + */ + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val shouldBeChecked = newItemSelected(newItemPosition) && !oldItemSelected(oldItemPosition) + val shouldBeUnchecked = !newItemSelected(newItemPosition) && oldItemSelected(oldItemPosition) + return CheckChanged(shouldBeChecked, shouldBeUnchecked, newHideCheckboxes) + } + + override fun getOldListSize(): Int = old.size + override fun getNewListSize(): Int = new.size + + private fun oldItemSelected(oldItemPosition: Int) = oldSelected.contains(old[oldItemPosition]) + private fun newItemSelected(newItemPosition: Int) = newSelected.contains(new[newItemPosition]) +} + +/** + * @property shouldBeChecked Item was previously unchecked and should be checked. + * @property shouldBeUnchecked Item was previously checked and should be unchecked. + * @property shouldHideCheckBox Checkbox should be visible. + */ +data class CheckChanged( + val shouldBeChecked: Boolean, + val shouldBeUnchecked: Boolean, + val shouldHideCheckBox: Boolean +) diff --git a/app/src/test/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapterTest.kt new file mode 100644 index 000000000..cc451b1ff --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapterTest.kt @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.collections + +import android.widget.FrameLayout +import androidx.core.view.isInvisible +import androidx.recyclerview.widget.RecyclerView +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import kotlinx.android.synthetic.main.collection_tab_list_row.* +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.home.Tab + +@RunWith(FenixRobolectricTestRunner::class) +class CollectionCreationTabListAdapterTest { + + private lateinit var interactor: CollectionCreationInteractor + private lateinit var adapter: CollectionCreationTabListAdapter + + @Before + fun setup() { + interactor = mockk() + adapter = CollectionCreationTabListAdapter(interactor) + + every { interactor.selectCollection(any(), any()) } just Runs + } + + @Test + fun `getItemCount should return the number of tab collections`() { + val tab = mockk() + + assertEquals(0, adapter.itemCount) + + adapter.updateData( + tabs = listOf(tab), + selectedTabs = emptySet() + ) + assertEquals(1, adapter.itemCount) + } + + @Test + fun `creates and binds viewholder`() { + val tab = mockk { + every { sessionId } returns "abc" + every { title } returns "Mozilla" + every { hostname } returns "mozilla.org" + every { url } returns "https://mozilla.org" + } + adapter.updateData( + tabs = listOf(tab), + selectedTabs = emptySet() + ) + + val holder = adapter.createViewHolder(FrameLayout(testContext), 0) + adapter.bindViewHolder(holder, 0) + + assertEquals("Mozilla", holder.tab_title.text) + assertEquals("mozilla.org", holder.hostname.text) + assertFalse(holder.tab_selected_checkbox.isInvisible) + assertTrue(holder.itemView.isClickable) + } + + @Test + fun `updateData inserts item`() { + val tab = mockk { + every { sessionId } returns "abc" + } + val observer = mockk(relaxed = true) + adapter.registerAdapterDataObserver(observer) + adapter.updateData( + tabs = listOf(tab), + selectedTabs = emptySet() + ) + + verify { observer.onItemRangeInserted(0, 1) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/collections/SaveCollectionListAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/collections/SaveCollectionListAdapterTest.kt new file mode 100644 index 000000000..ddcff8572 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/collections/SaveCollectionListAdapterTest.kt @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.collections + +import android.view.ViewGroup +import android.widget.FrameLayout +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import kotlinx.android.synthetic.main.collections_list_item.view.* +import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class SaveCollectionListAdapterTest { + + private lateinit var parent: ViewGroup + private lateinit var interactor: CollectionCreationInteractor + private lateinit var adapter: SaveCollectionListAdapter + + @Before + fun setup() { + parent = FrameLayout(testContext) + interactor = mockk() + adapter = SaveCollectionListAdapter(interactor) + + every { interactor.selectCollection(any(), any()) } just Runs + } + + @Test + fun `getItemCount should return the number of tab collections`() { + val collection = mockk() + + assertEquals(0, adapter.itemCount) + + adapter.updateData( + tabCollections = listOf(collection), + selectedTabs = emptySet() + ) + assertEquals(1, adapter.itemCount) + } + + @Test + fun `creates and binds viewholder`() { + val collection = mockk { + every { id } returns 0L + every { title } returns "Collection" + every { tabs } returns listOf( + mockk { + every { url } returns "https://mozilla.org" + }, + mockk { + every { url } returns "https://firefox.com" + } + ) + } + adapter.updateData( + tabCollections = listOf(collection), + selectedTabs = emptySet() + ) + + val holder = adapter.createViewHolder(parent, 0) + adapter.bindViewHolder(holder, 0) + + assertEquals("Collection", holder.itemView.collection_item.text) + assertEquals("mozilla.org, firefox.com", holder.itemView.collection_description.text) + + holder.itemView.performClick() + verify { interactor.selectCollection(collection, emptyList()) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/collections/TabDiffUtilTest.kt b/app/src/test/java/org/mozilla/fenix/collections/TabDiffUtilTest.kt new file mode 100644 index 000000000..748dbde12 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/collections/TabDiffUtilTest.kt @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.collections + +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mozilla.fenix.home.Tab + +class TabDiffUtilTest { + + @Test + fun `list size is returned`() { + val diffUtil = TabDiffUtil( + old = listOf(mockk(), mockk()), + new = listOf(mockk()), + oldSelected = emptySet(), + newSelected = emptySet(), + oldHideCheckboxes = false, + newHideCheckboxes = false + ) + + assertEquals(2, diffUtil.oldListSize) + assertEquals(1, diffUtil.newListSize) + } + + @Test + fun `single lists are the same`() { + val tab = mockk { + every { sessionId } returns "abc" + } + val diffUtil = TabDiffUtil( + old = listOf(tab), + new = listOf(tab), + oldSelected = emptySet(), + newSelected = emptySet(), + oldHideCheckboxes = false, + newHideCheckboxes = false + ) + + assertTrue(diffUtil.areItemsTheSame(0, 0)) + assertTrue(diffUtil.areContentsTheSame(0, 0)) + } + + @Test + fun `selection affects contents`() { + val tab = mockk { + every { sessionId } returns "abc" + } + val diffUtil = TabDiffUtil( + old = listOf(tab), + new = listOf(tab), + oldSelected = emptySet(), + newSelected = setOf(tab), + oldHideCheckboxes = false, + newHideCheckboxes = false + ) + + assertTrue(diffUtil.areItemsTheSame(0, 0)) + assertFalse(diffUtil.areContentsTheSame(0, 0)) + } + + @Test + fun `hide checkboxes affects contents`() { + val tab = mockk { + every { sessionId } returns "abc" + } + val diffUtil = TabDiffUtil( + old = listOf(tab), + new = listOf(tab), + oldSelected = setOf(tab), + newSelected = setOf(tab), + oldHideCheckboxes = false, + newHideCheckboxes = true + ) + + assertTrue(diffUtil.areItemsTheSame(0, 0)) + assertFalse(diffUtil.areContentsTheSame(0, 0)) + } + + @Test + fun `change payload covers no change case`() { + val tab = mockk() + val payload = TabDiffUtil( + old = listOf(tab), + new = listOf(tab), + oldSelected = setOf(tab), + newSelected = setOf(tab), + oldHideCheckboxes = false, + newHideCheckboxes = false + ).getChangePayload(0, 0) + + assertEquals( + CheckChanged( + shouldBeChecked = false, + shouldBeUnchecked = false, + shouldHideCheckBox = false + ), + payload + ) + } + + @Test + fun `include shouldBeChecked in change payload`() { + val tab = mockk() + val payload = TabDiffUtil( + old = listOf(tab), + new = listOf(tab), + oldSelected = emptySet(), + newSelected = setOf(tab), + oldHideCheckboxes = false, + newHideCheckboxes = false + ).getChangePayload(0, 0) + + assertEquals( + CheckChanged( + shouldBeChecked = true, + shouldBeUnchecked = false, + shouldHideCheckBox = false + ), + payload + ) + } + + @Test + fun `include shouldBeUnchecked in change payload`() { + val tab = mockk() + val payload = TabDiffUtil( + old = listOf(tab), + new = listOf(tab), + oldSelected = setOf(tab), + newSelected = emptySet(), + oldHideCheckboxes = false, + newHideCheckboxes = true + ).getChangePayload(0, 0) + + assertEquals( + CheckChanged( + shouldBeChecked = false, + shouldBeUnchecked = true, + shouldHideCheckBox = true + ), + payload + ) + } +}