From a5c5f177ed69388d138992d4046895bad842fde0 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 30 May 2024 00:49:51 +0200 Subject: [PATCH] Add UI tests for new save and restore classes --- .../test/TestNestedListFragment.kt | 38 +++++++-- .../tests/selection/SaveRestoreStateTest.kt | 82 ++++++++++++++----- 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/TestNestedListFragment.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/TestNestedListFragment.kt index 23d003f3..57b366be 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/TestNestedListFragment.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/TestNestedListFragment.kt @@ -25,6 +25,8 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.rubensousa.dpadrecyclerview.DpadRecyclerView import com.rubensousa.dpadrecyclerview.OnViewFocusedListener +import com.rubensousa.dpadrecyclerview.state.DpadScrollState +import com.rubensousa.dpadrecyclerview.state.DpadStateRegistry import com.rubensousa.dpadrecyclerview.test.tests.AbstractTestAdapter import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent @@ -34,6 +36,7 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container itemLayoutId = R.layout.dpadrecyclerview_item_horizontal, numberOfItems = 200 ) + private val stateRegistry = DpadStateRegistry(this) private val parentFocusEvents = arrayListOf() private val childFocusEvents = arrayListOf() private val parentFocusListener = object : OnViewFocusedListener { @@ -52,7 +55,11 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container childFocusEvents.add(DpadFocusEvent(parent, child, parent.layoutPosition)) } } - private val nestedAdapter = NestedAdapter(configuration, childFocusListener) + private val nestedAdapter = NestedAdapter( + configuration = configuration, + onViewFocusedListener = childFocusListener, + scrollState = stateRegistry.getScrollState() + ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -70,7 +77,8 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container class NestedAdapter( private val configuration: TestAdapterConfiguration, - private val onViewFocusedListener: OnViewFocusedListener + private val onViewFocusedListener: OnViewFocusedListener, + private val scrollState: DpadScrollState, ) : AbstractTestAdapter(configuration) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { @@ -78,16 +86,29 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container LayoutInflater.from(parent.context) .inflate(R.layout.dpadrecyclerview_nested_list, parent, false), configuration, - onViewFocusedListener + onViewFocusedListener, ) } override fun onBindViewHolder(holder: ListViewHolder, position: Int) { holder.bind(position) + scrollState.restore( + recyclerView = holder.recyclerView, + key = position.toString(), + adapter = holder.adapter + ) } - } + override fun onViewRecycled(holder: ListViewHolder) { + super.onViewRecycled(holder) + scrollState.save( + recyclerView = holder.recyclerView, + key = holder.absoluteAdapterPosition.toString(), + detachAdapter = true + ) + } + } class ListViewHolder( val view: View, @@ -95,23 +116,24 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container onViewFocusedListener: OnViewFocusedListener, ) : RecyclerView.ViewHolder(view) { - private val adapter = TestAdapter( + val adapter = TestAdapter( adapterConfiguration = configuration, onViewHolderSelected = { position -> }, onViewHolderDeselected = { position -> } ) private val textView = view.findViewById(R.id.textView) - private val recyclerView = view.findViewById(R.id.nestedRecyclerView) + + val recyclerView = view.findViewById(R.id.nestedRecyclerView) init { - recyclerView.adapter = adapter recyclerView.addOnViewFocusedListener(onViewFocusedListener) } fun bind(position: Int) { + recyclerView.tag = position textView.text = "List $position" + textView.freezesText = true } - } } diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SaveRestoreStateTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SaveRestoreStateTest.kt index 96b67b14..a1f1f452 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SaveRestoreStateTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/selection/SaveRestoreStateTest.kt @@ -16,44 +16,40 @@ package com.rubensousa.dpadrecyclerview.test.tests.selection -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withTagValue import com.google.common.truth.Truth.assertThat -import com.rubensousa.dpadrecyclerview.ChildAlignment -import com.rubensousa.dpadrecyclerview.ParentAlignment -import com.rubensousa.dpadrecyclerview.ParentAlignment.Edge -import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration +import com.rubensousa.dpadrecyclerview.test.TestNestedListFragment import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusPosition import com.rubensousa.dpadrecyclerview.test.helpers.assertOnRecyclerView import com.rubensousa.dpadrecyclerview.test.helpers.getItemViewBounds import com.rubensousa.dpadrecyclerview.test.helpers.getRecyclerViewBounds -import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest +import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition import com.rubensousa.dpadrecyclerview.testing.KeyEvents +import com.rubensousa.dpadrecyclerview.testing.R +import com.rubensousa.dpadrecyclerview.testing.assertions.DpadRecyclerViewAssertions import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf import org.junit.Before import org.junit.Rule import org.junit.Test -class SaveRestoreStateTest : DpadRecyclerViewTest() { +class SaveRestoreStateTest { @get:Rule val idleTimeoutRule = DisableIdleTimeoutRule() - override fun getDefaultLayoutConfiguration(): TestLayoutConfiguration { - return TestLayoutConfiguration( - spans = 1, - orientation = RecyclerView.VERTICAL, - parentAlignment = ParentAlignment( - edge = Edge.MIN_MAX - ), - childAlignment = ChildAlignment(offset = 0) - ) - } + private lateinit var fragmentScenario: FragmentScenario @Before fun setup() { - launchFragment() + fragmentScenario = launchFragment() } @Test @@ -61,7 +57,7 @@ class SaveRestoreStateTest : DpadRecyclerViewTest() { KeyEvents.pressDown(times = 5) assertFocusPosition(5) - recreateFragment() + fragmentScenario.recreate() val recyclerViewBounds = getRecyclerViewBounds() val viewBounds = getItemViewBounds(position = 5) @@ -70,4 +66,52 @@ class SaveRestoreStateTest : DpadRecyclerViewTest() { assertFocusPosition(5) } + @Test + fun testSelectionStateAcrossNestedListsIsSaved() { + // given + KeyEvents.pressRight(times = 5) + + // when + KeyEvents.pressDown(times = 25) + KeyEvents.pressUp(times = 25) + + // then + Espresso.onView( + allOf( + withId(com.rubensousa.dpadrecyclerview.test.R.id.nestedRecyclerView), + withTagValue(Matchers.`is`(0)) + ) + ).check(DpadRecyclerViewAssertions.isSelected(position = 5)) + } + + @Test + fun testSelectionStateAcrossNestedListsSurvivesConfigurationChanges() { + // given + KeyEvents.pressRight(times = 5) + KeyEvents.pressDown(times = 25) + KeyEvents.pressUp(times = 25) + + // when + fragmentScenario.recreate() + + // then + Espresso.onView( + allOf( + withId(com.rubensousa.dpadrecyclerview.test.R.id.nestedRecyclerView), + withTagValue(Matchers.`is`(0)) + ) + ).check(DpadRecyclerViewAssertions.isSelected(position = 5)) + } + + private fun launchFragment(): FragmentScenario { + return launchFragmentInContainer( + themeResId = R.style.DpadRecyclerViewTestTheme + ).also { + fragmentScenario = it + waitForCondition("Waiting for layout pass") { recyclerView -> + !recyclerView.isLayoutRequested + } + } + } + }