From d2656f371a65df223a28ce246eccb4ff45d2fb84 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 14 Nov 2024 17:55:53 +0100 Subject: [PATCH 1/4] Add test for sub selection --- .../test/tests/selection/SubSelectionTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SubSelectionTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SubSelectionTest.kt index 7dbe793a..a8ae79ac 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SubSelectionTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SubSelectionTest.kt @@ -39,9 +39,12 @@ import com.rubensousa.dpadrecyclerview.test.TestViewHolder import com.rubensousa.dpadrecyclerview.test.assertions.ViewHolderAlignmentCountAssertion import com.rubensousa.dpadrecyclerview.test.assertions.ViewHolderSelectionCountAssertion import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusAndSelection +import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView +import com.rubensousa.dpadrecyclerview.test.helpers.runOnMainThread import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition import com.rubensousa.dpadrecyclerview.test.helpers.selectSubPosition import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState +import com.rubensousa.dpadrecyclerview.test.helpers.waitForLayout import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest import com.rubensousa.dpadrecyclerview.testfixtures.DpadSelectionEvent import com.rubensousa.dpadrecyclerview.testing.KeyEvents @@ -175,6 +178,32 @@ class SubSelectionTest : DpadRecyclerViewTest() { ) } + @Test + fun testSubSelectionIsAppliedForNextLayout() = report { + val position = 10 + val subPosition = 1 + param("position", position.toString()) + param("subPosition", subPosition.toString()) + + var recyclerView: DpadRecyclerView? = null + Given("Launch fragment") { + launchSubPositionFragment() + onRecyclerView("Retrieve recyclerView instance") { + recyclerView = it + } + } + + When("Select target positions") { + runOnMainThread { + recyclerView?.setSelectedSubPosition(position, subPosition) + } + waitForLayout() + } + + Then("Assert selection is at position $position and sub position $subPosition") { + assertFocusAndSelection(position, subPosition) + } + } private fun getSelectionsFromTasks(): List { var events = listOf() From 42c1607ed174bb59dd485d29c1d96df523500a0f Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 14 Nov 2024 18:02:53 +0100 Subject: [PATCH 2/4] Fix concurrent modification exceptions when listeners remove themselves inside the callbacks --- .../test/helpers/TestExtensions.kt | 25 ++++ .../test/tests/layout/LayoutCompletionTest.kt | 129 ++++++++++++++++++ .../layoutmanager/layout/PivotLayout.kt | 13 +- 3 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/helpers/TestExtensions.kt create mode 100644 dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutCompletionTest.kt diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/helpers/TestExtensions.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/helpers/TestExtensions.kt new file mode 100644 index 00000000..219ec35c --- /dev/null +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/helpers/TestExtensions.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Rúben Sousa + * + * 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 com.rubensousa.dpadrecyclerview.test.helpers + +import androidx.test.platform.app.InstrumentationRegistry + +fun runOnMainThread(action: () -> Unit) { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + action() + } +} diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutCompletionTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutCompletionTest.kt new file mode 100644 index 00000000..240e8b58 --- /dev/null +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutCompletionTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2024 Rúben Sousa + * + * 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 com.rubensousa.dpadrecyclerview.test.tests.layout + +import androidx.recyclerview.widget.RecyclerView +import com.google.common.truth.Truth.assertThat +import com.rubensousa.dpadrecyclerview.ChildAlignment +import com.rubensousa.dpadrecyclerview.DpadRecyclerView +import com.rubensousa.dpadrecyclerview.OnChildLaidOutListener +import com.rubensousa.dpadrecyclerview.ParentAlignment +import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration +import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView +import com.rubensousa.dpadrecyclerview.test.helpers.runOnMainThread +import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition +import com.rubensousa.dpadrecyclerview.test.helpers.waitForLayout +import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest +import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class LayoutCompletionTest : DpadRecyclerViewTest() { + + @get:Rule + val idleTimeoutRule = DisableIdleTimeoutRule() + + override fun getDefaultLayoutConfiguration(): TestLayoutConfiguration { + return TestLayoutConfiguration( + spans = 1, + orientation = RecyclerView.VERTICAL, + parentAlignment = ParentAlignment( + edge = ParentAlignment.Edge.NONE, + fraction = 0.0f + ), + childAlignment = ChildAlignment( + fraction = 0.0f + ) + ) + } + + @Before + fun setup() { + launchFragment() + } + + @Test + fun testLayoutCompleteListenerThatRemovesItself() = report { + var currentRecyclerView: DpadRecyclerView? = null + var events = 0 + val listeners = 3 + Given("Attach listener that removes itself") { + onRecyclerView("Get recyclerview") { recyclerView -> + currentRecyclerView = recyclerView + } + runOnMainThread { + // Add listeners that remove themselves + repeat(listeners) { + currentRecyclerView!!.addOnLayoutCompletedListener( + object : DpadRecyclerView.OnLayoutCompletedListener { + override fun onLayoutCompleted(state: RecyclerView.State) { + currentRecyclerView.removeOnLayoutCompletedListener(this) + events++ + } + } + ) + } + } + } + + When("Request layout") { + selectPosition(10) + waitForLayout() + } + + Then("Listener got removed") { + assertThat(events).isEqualTo(listeners) + } + } + + @Test + fun testChildLayoutListenerThatRemovesItself() = report { + var currentRecyclerView: DpadRecyclerView? = null + var events = 0 + val listeners = 3 + Given("Attach listener that removes itself") { + onRecyclerView("Get recyclerview") { recyclerView -> + currentRecyclerView = recyclerView + } + runOnMainThread { + // Add listeners that remove themselves + repeat(listeners) { + currentRecyclerView!!.addOnChildLaidOutListener(object : OnChildLaidOutListener { + override fun onChildLaidOut( + parent: RecyclerView, + child: RecyclerView.ViewHolder + ) { + currentRecyclerView.removeOnChildLaidOutListener(this) + events++ + } + }) + } + } + } + + When("Request layout") { + selectPosition(10) + waitForLayout() + } + + Then("Listener got removed") { + assertThat(events).isEqualTo(listeners) + } + } + +} \ No newline at end of file diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt index 8627998d..5c2e2b04 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt @@ -48,7 +48,8 @@ internal class PivotLayout( private val childLayoutListener = ChildLayoutListener() private val childLaidOutListeners = mutableListOf() private var structureEngineer = createStructureEngineer() - private val layoutCompleteListeners = ArrayList() + private val layoutCompleteListeners = + mutableListOf() private var anchor: Int? = null private var initialSelectionPending = false @@ -194,8 +195,8 @@ internal class PivotLayout( updateInitialSelection() } layoutInfo.onLayoutCompleted() - layoutCompleteListeners.forEach { listener -> - listener.onLayoutCompleted(state) + for (i in layoutCompleteListeners.size - 1 downTo 0) { + layoutCompleteListeners[i].onLayoutCompleted(state) } } @@ -234,7 +235,7 @@ internal class PivotLayout( } fun removeOnChildLaidOutListener(listener: OnChildLaidOutListener) { - childLaidOutListeners.add(listener) + childLaidOutListeners.remove(listener) } fun clearOnChildLaidOutListeners() { @@ -381,8 +382,8 @@ internal class PivotLayout( val recyclerView = layoutInfo.getRecyclerView() ?: return val viewHolder = layoutInfo.getChildViewHolder(view) ?: return - childLaidOutListeners.forEach { listener -> - listener.onChildLaidOut(recyclerView, viewHolder) + for (i in childLaidOutListeners.size - 1 downTo 0) { + childLaidOutListeners[i].onChildLaidOut(recyclerView, viewHolder) } } From f319ad5c3562c501e182058a3abf79b350937195 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 14 Nov 2024 19:27:14 +0100 Subject: [PATCH 3/4] Fix other listeners iteration order to protect against listeners that remove themselves --- .../test/tests/focus/FocusListenerTest.kt | 28 +++++++++++++++ .../test/tests/selection/SelectionTest.kt | 35 +++++++++++++++++++ .../dpadrecyclerview/DpadRecyclerView.kt | 3 +- .../layoutmanager/ListExtension.kt | 23 ++++++++++++ .../layoutmanager/PivotSelector.kt | 16 ++++----- .../layoutmanager/layout/PivotLayout.kt | 10 +++--- 6 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtension.kt diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/FocusListenerTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/FocusListenerTest.kt index 2a113120..45183ecf 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/FocusListenerTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/focus/FocusListenerTest.kt @@ -30,6 +30,7 @@ import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState +import com.rubensousa.dpadrecyclerview.test.helpers.waitForLayout import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule @@ -256,4 +257,31 @@ class FocusListenerTest : DpadRecyclerViewTest() { assertThat(events).isEqualTo(0) } + @Test + fun testFocusListenerThatRemovesItself() = report { + val listeners = 5 + var events = 0 + Given("Attach listeners") { + launchFragment() + onRecyclerView("Get recyclerView") { recyclerView -> + repeat(listeners) { + recyclerView.addOnViewFocusedListener(object : OnViewFocusedListener { + override fun onViewFocused(parent: RecyclerView.ViewHolder, child: View) { + events++ + recyclerView.removeOnViewFocusedListener(this) + } + }) + } + } + } + When("Select another position") { + selectPosition(2) + waitForLayout() + } + + Then("Events are received") { + assertThat(events).isEqualTo(listeners) + } + } + } diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SelectionTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SelectionTest.kt index 0de9a5ee..1c5fa149 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SelectionTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SelectionTest.kt @@ -22,6 +22,7 @@ import androidx.test.espresso.matcher.ViewMatchers import com.google.common.truth.Truth.assertThat import com.rubensousa.dpadrecyclerview.ChildAlignment import com.rubensousa.dpadrecyclerview.DpadRecyclerView +import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener import com.rubensousa.dpadrecyclerview.ParentAlignment import com.rubensousa.dpadrecyclerview.ParentAlignment.Edge import com.rubensousa.dpadrecyclerview.test.TestAdapterConfiguration @@ -337,4 +338,38 @@ class SelectionTest : DpadRecyclerViewTest() { } } + @Test + fun testSelectionListenerThatRemovesItself() = report { + val listeners = 5 + var events = 0 + Given("Attach listeners") { + launchFragment() + onRecyclerView("Get recyclerView") { recyclerView -> + repeat(listeners) { + recyclerView.addOnViewHolderSelectedListener(object : + OnViewHolderSelectedListener { + override fun onViewHolderSelected( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) { + super.onViewHolderSelected(parent, child, position, subPosition) + recyclerView.removeOnViewHolderSelectedListener(this) + events++ + } + }) + } + } + } + When("Select another position") { + selectPosition(10) + waitForLayout() + } + + Then("Events are received") { + assertThat(events).isEqualTo(listeners) + } + } + } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt index 50e38901..e9cfef65 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt @@ -36,6 +36,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import com.rubensousa.dpadrecyclerview.layoutmanager.PivotLayoutManager import com.rubensousa.dpadrecyclerview.layoutmanager.focus.GlobalFocusChangeListener +import com.rubensousa.dpadrecyclerview.layoutmanager.forEachReversed import com.rubensousa.dpadrecyclerview.spacing.DpadGridSpacingDecoration import com.rubensousa.dpadrecyclerview.spacing.DpadLinearSpacingDecoration import com.rubensousa.dpadrecyclerview.spacing.DpadSpacingDecoration @@ -88,7 +89,7 @@ open class DpadRecyclerView @JvmOverloads constructor( } private val globalFocusChangeListener by lazy { GlobalFocusChangeListener(this) { - focusLossListeners.forEach { listener -> + focusLossListeners.forEachReversed { listener -> listener.onFocusLost(this) } } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtension.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtension.kt new file mode 100644 index 00000000..e5cb241a --- /dev/null +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtension.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Rúben Sousa + * + * 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 com.rubensousa.dpadrecyclerview.layoutmanager + +internal inline fun List.forEachReversed(action: (T) -> Unit) { + for (i in size - 1 downTo 0) { + action(get(i)) + } +} diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt index deec0392..a5ce3da5 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt @@ -132,7 +132,7 @@ internal class PivotSelector( } } val focusedViewHolder = currentRecyclerView.findContainingViewHolder(view) ?: return - focusListeners.forEach { listener -> + focusListeners.forEachReversed { listener -> listener.onViewFocused( parent = focusedViewHolder, child = view, @@ -152,7 +152,7 @@ internal class PivotSelector( pendingChildFocus = view return } - focusListeners.forEach { listener -> + focusListeners.forEachReversed { listener -> listener.onViewFocused( parent = parentViewHolder, child = view @@ -346,7 +346,7 @@ internal class PivotSelector( if (viewHolder is DpadViewHolder) { viewHolder.onViewHolderDeselected() } - selectionListeners.forEach { listener -> + selectionListeners.forEachReversed { listener -> listener.onViewHolderDeselected(recyclerView, viewHolder) } } @@ -358,13 +358,13 @@ internal class PivotSelector( selectedViewHolder = viewHolder if (viewHolder != null) { - selectionListeners.forEach { listener -> + selectionListeners.forEachReversed { listener -> listener.onViewHolderSelected( recyclerView, viewHolder, position, subPosition ) } } else { - selectionListeners.forEach { listener -> + selectionListeners.forEachReversed { listener -> listener.onViewHolderSelected( recyclerView, null, RecyclerView.NO_POSITION, 0 ) @@ -406,13 +406,13 @@ internal class PivotSelector( } if (viewHolder != null) { - selectionListeners.forEach { listener -> + selectionListeners.forEachReversed { listener -> listener.onViewHolderSelectedAndAligned( recyclerView, viewHolder, position, subPosition ) } } else { - selectionListeners.forEach { listener -> + selectionListeners.forEachReversed { listener -> listener.onViewHolderSelectedAndAligned( recyclerView, null, RecyclerView.NO_POSITION, 0 ) @@ -459,7 +459,7 @@ internal class PivotSelector( viewHolder.onViewHolderDeselected() } recyclerView?.let { - selectionListeners.forEach { listener -> + selectionListeners.forEachReversed { listener -> listener.onViewHolderDeselected(it, viewHolder) } } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt index 5c2e2b04..d39d59e3 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt @@ -28,6 +28,7 @@ import com.rubensousa.dpadrecyclerview.layoutmanager.DpadLayoutParams import com.rubensousa.dpadrecyclerview.layoutmanager.LayoutConfiguration import com.rubensousa.dpadrecyclerview.layoutmanager.PivotSelector import com.rubensousa.dpadrecyclerview.layoutmanager.alignment.LayoutAlignment +import com.rubensousa.dpadrecyclerview.layoutmanager.forEachReversed import com.rubensousa.dpadrecyclerview.layoutmanager.layout.grid.GridLayoutEngineer import com.rubensousa.dpadrecyclerview.layoutmanager.layout.linear.LinearLayoutEngineer import com.rubensousa.dpadrecyclerview.layoutmanager.scroll.LayoutScroller @@ -195,8 +196,8 @@ internal class PivotLayout( updateInitialSelection() } layoutInfo.onLayoutCompleted() - for (i in layoutCompleteListeners.size - 1 downTo 0) { - layoutCompleteListeners[i].onLayoutCompleted(state) + layoutCompleteListeners.forEachReversed { listener -> + listener.onLayoutCompleted(state) } } @@ -381,9 +382,8 @@ internal class PivotLayout( } val recyclerView = layoutInfo.getRecyclerView() ?: return val viewHolder = layoutInfo.getChildViewHolder(view) ?: return - - for (i in childLaidOutListeners.size - 1 downTo 0) { - childLaidOutListeners[i].onChildLaidOut(recyclerView, viewHolder) + childLaidOutListeners.forEachReversed { listener -> + listener.onChildLaidOut(recyclerView, viewHolder) } } From b85ba5d84c17169916217c0574b38d27597d4a11 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 14 Nov 2024 19:37:32 +0100 Subject: [PATCH 4/4] Add unit tests for list extension --- .../layoutmanager/ListExtensionTest.kt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtensionTest.kt diff --git a/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtensionTest.kt b/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtensionTest.kt new file mode 100644 index 00000000..33dd87a3 --- /dev/null +++ b/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/layoutmanager/ListExtensionTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Rúben Sousa + * + * 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 com.rubensousa.dpadrecyclerview.layoutmanager + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ListExtensionTest { + + @Test + fun `empty list does nothing`() { + // given + val list = emptyList() + var invoked = false + + // when + list.forEachReversed { + invoked = true + } + + // then + assertThat(invoked).isFalse() + } + + @Test + fun `removing list within itself works`() { + // given + val list = mutableListOf("value1", "value2", "value3") + + // when + list.forEachReversed { value -> + list.remove(value) + } + + // then + assertThat(list).isEmpty() + } + + @Test + fun `values are iterated in reverse order`() { + // given + val list = mutableListOf("value1", "value2", "value3") + val iterated = mutableListOf() + + // when + list.forEachReversed { value -> + iterated.add(value) + } + + // then + assertThat(iterated).isEqualTo(list.reversed()) + } + +}