From 5dfbb5e36ae7de2770880b37939cc2ce018ff43a Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 01:06:00 +0200 Subject: [PATCH 01/20] Remove idle timeout since it adds flakiness in API 22 --- .../compose/DpadComposeFocusViewHolderTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt index 23d4a1aa..9eb07297 100644 --- a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt +++ b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt @@ -37,15 +37,11 @@ import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions import com.rubensousa.dpadrecyclerview.testing.actions.DpadViewActions -import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule import org.junit.Rule import org.junit.Test class DpadComposeFocusViewHolderTest { - @get:Rule - val idleTimeoutRule = DisableIdleTimeoutRule() - @get:Rule val screenRecorderRule = ScreenRecorderRule() From ee9a03dabe1c3a7682800cd0829e11fb12a9d243 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 01:06:20 +0200 Subject: [PATCH 02/20] Include all APIs in tests again --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c6b8d4bb..a7f4c855 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -49,7 +49,7 @@ jobs: matrix: arch: [ x86 ] target: [ android-tv ] - api-level: [27] + api-level: [22, 25, 27, 28, 29, 30, 31] profile: [tv_1080p] steps: - name: checkout From 78456f633681f2812edfc89a8c8692b8e4b952e2 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 01:57:48 +0200 Subject: [PATCH 03/20] Update to AGP 8.5.0 --- gradle/libs.versions.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2671565c..1d0bfd86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,8 @@ [versions] -android-gradle-plugin = "8.4.2" +android-gradle-plugin = "8.5.0" androidx-appcompat = "1.7.0" androidx-collection = "1.4.0" androidx-concurrent-futures = "1.2.0" -androidx-compose-compiler = "1.5.10" androidx-compose-material3 = '1.2.1' androidx-compose-ui = "1.7.0-beta03" androidx-constraintlayout = "2.1.4" From 0a65b627883442974943535a48424ede9d3a3b67 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 01:58:05 +0200 Subject: [PATCH 04/20] Add sample for vertical list --- .../ui/screen/list/ShortListFragment.kt | 2 +- .../ui/screen/list/VerticalListFragment.kt | 88 +++++++++++++++++++ .../sample/ui/screen/main/MainViewModel.kt | 4 + sample/src/main/res/navigation/nav_graph.xml | 8 ++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/VerticalListFragment.kt diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt index 0bfb6fde..7d6588c5 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt @@ -47,7 +47,7 @@ class ShortListFragment : Fragment(R.layout.screen_recyclerview) { } } - internal class Adapter( + private class Adapter( private val items: List, ) : RecyclerView.Adapter() { diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/VerticalListFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/VerticalListFragment.kt new file mode 100644 index 00000000..8b7f9fae --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/VerticalListFragment.kt @@ -0,0 +1,88 @@ +/* + * 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.sample.ui.screen.list + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.dpadrecyclerview.ParentAlignment +import com.rubensousa.dpadrecyclerview.compose.DpadComposeFocusViewHolder +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenRecyclerviewBinding +import com.rubensousa.dpadrecyclerview.sample.ui.viewBinding +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemComposable + +class VerticalListFragment : Fragment(R.layout.screen_recyclerview) { + + private val binding by viewBinding(ScreenRecyclerviewBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.recyclerView.apply { + setParentAlignment( + ParentAlignment( + edge = ParentAlignment.Edge.MIN_MAX, + fraction = 0.5f + ) + ) + adapter = Adapter( + items = List(50) { i -> i } + ) + requestFocus() + } + } + + private class Adapter( + private val items: List, + ) : RecyclerView.Adapter>() { + + override fun onCreateViewHolder( + parent: ViewGroup, viewType: Int + ): DpadComposeFocusViewHolder { + return DpadComposeFocusViewHolder(parent) { item -> + ItemComposable( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + item = item, + onClick = { + + } + ) + } + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: DpadComposeFocusViewHolder, position: Int) { + holder.setItemState(items[position]) + } + + override fun onViewRecycled(holder: DpadComposeFocusViewHolder) { + super.onViewRecycled(holder) + holder.setItemState(null) + } + + + } +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt index c2a78987..bb7bb76a 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt @@ -61,6 +61,10 @@ class MainViewModel : ViewModel() { direction = MainFragmentDirections.openList(enableLooping = true), title = "Infinite lists (loop)" ), + ScreenDestination( + direction = MainFragmentDirections.openVerticalList(), + title = "Vertical list" + ), ScreenDestination( direction = MainFragmentDirections.openShortList(), title = "Short list" diff --git a/sample/src/main/res/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index 9d8cfb3b..ab321229 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -92,8 +92,16 @@ android:id="@+id/open_span_header" app:destination="@id/span_header_fragment" /> + + + + From 3e8d40a7e02a667f12c185601287dc7ec9fc05f5 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 02:00:23 +0200 Subject: [PATCH 05/20] Do not allow sending focus to ComposeView directly --- .../compose/DpadComposeFocusViewHolderTest.kt | 16 +++++++-- .../compose/DpadComposeViewHolderTest.kt | 6 ++-- .../compose/test/ComposeFocusTestActivity.kt | 35 ++++++++++--------- .../compose/DpadComposeFocusViewHolder.kt | 2 -- .../compose/DpadComposeView.kt | 4 +-- .../common/ComposePlaceholderAdapter.kt | 4 +++ 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt index 9eb07297..03a1b605 100644 --- a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt +++ b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt @@ -37,6 +37,7 @@ import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions import com.rubensousa.dpadrecyclerview.testing.actions.DpadViewActions +import org.junit.Before import org.junit.Rule import org.junit.Test @@ -48,6 +49,11 @@ class DpadComposeFocusViewHolderTest { @get:Rule val composeTestRule = createAndroidComposeRule() + @Before + fun setup() { + composeTestRule.waitForIdle() + } + @Test fun testFirstItemHasFocus() { assertFocus(item = 0, isFocused = true) @@ -109,6 +115,7 @@ class DpadComposeFocusViewHolderTest { @Test fun testCompositionIsNotClearedWhenDetachingFromWindow() { + // given composeTestRule.activityRule.scenario.onActivity { activity -> activity.getRecyclerView() .setExtraLayoutSpaceStrategy(object : ExtraLayoutSpaceStrategy { @@ -117,19 +124,24 @@ class DpadComposeFocusViewHolderTest { } }) } + + // when repeat(3) { KeyEvents.pressDown() waitForIdleScroll() } + // then composeTestRule.onNodeWithText("0").assertExists() composeTestRule.onNodeWithText("0").assertIsNotDisplayed() } @Test fun testCompositionIsClearedWhenViewHolderIsRecycled() { - KeyEvents.pressDown(times = 10) - waitForIdleScroll() + repeat(times = 10) { + KeyEvents.pressDown() + waitForIdleScroll() + } composeTestRule.onNodeWithText("0").assertDoesNotExist() diff --git a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt index 049320f6..f801d6f6 100644 --- a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt +++ b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt @@ -131,8 +131,10 @@ class DpadComposeViewHolderTest { @Test fun testCompositionIsClearedWhenViewHolderIsRecycled() { - KeyEvents.pressDown(times = 10) - waitForIdleScroll() + repeat(times = 10) { + KeyEvents.pressDown() + waitForIdleScroll() + } composeTestRule.onNodeWithText("0").assertDoesNotExist() diff --git a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/test/ComposeFocusTestActivity.kt b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/test/ComposeFocusTestActivity.kt index 8f372583..6d9ff77d 100644 --- a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/test/ComposeFocusTestActivity.kt +++ b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/test/ComposeFocusTestActivity.kt @@ -98,23 +98,20 @@ class ComposeFocusTestActivity : AppCompatActivity() { parent: ViewGroup, viewType: Int ): DpadComposeFocusViewHolder { - return DpadComposeFocusViewHolder( - parent = parent, - content = { item -> - TestComposableFocus( - modifier = Modifier - .fillMaxWidth() - .height(150.dp), - item = item, - onClick = { - clicks.add(item) - }, - onDispose = { - onDispose(item) - } - ) - }, - ) + return DpadComposeFocusViewHolder(parent) { item -> + TestComposableFocus( + modifier = Modifier + .fillMaxWidth() + .height(150.dp), + item = item, + onClick = { + clicks.add(item) + }, + onDispose = { + onDispose(item) + } + ) + } } override fun getItemCount(): Int = items.size @@ -123,5 +120,9 @@ class ComposeFocusTestActivity : AppCompatActivity() { holder.setItemState(items[position]) } + override fun onViewRecycled(holder: DpadComposeFocusViewHolder) { + holder.setItemState(null) + } + } } \ No newline at end of file diff --git a/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt b/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt index cb477c9d..1b850d32 100644 --- a/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt +++ b/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt @@ -71,8 +71,6 @@ class DpadComposeFocusViewHolder( fun setFocusable(focusable: Boolean) { composeView.apply { - isFocusable = focusable - isFocusableInTouchMode = focusable descendantFocusability = if (focusable) { ViewGroup.FOCUS_AFTER_DESCENDANTS } else { diff --git a/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeView.kt b/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeView.kt index 9f91935c..9617ebc1 100644 --- a/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeView.kt +++ b/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeView.kt @@ -41,6 +41,8 @@ internal class DpadComposeView @JvmOverloads constructor( init { addView(composeView) clipChildren = false + composeView.isFocusable = false + composeView.isFocusableInTouchMode = false super.setOnFocusChangeListener(internalFocusListener) } @@ -53,8 +55,6 @@ internal class DpadComposeView @JvmOverloads constructor( dispatchFocusToComposable: Boolean ) { if (dispatchFocusToComposable) { - composeView.isFocusable = isFocusable - composeView.isFocusableInTouchMode = isFocusable composeView.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS this.isFocusable = false this.isFocusableInTouchMode = false diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/ComposePlaceholderAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/ComposePlaceholderAdapter.kt index 7b5b9fe4..82b6aac9 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/ComposePlaceholderAdapter.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/ComposePlaceholderAdapter.kt @@ -55,6 +55,10 @@ class ComposePlaceholderAdapter( holder.setItemState(true) } + override fun onViewRecycled(holder: DpadComposeViewHolder) { + holder.setItemState(null) + } + fun show(enabled: Boolean) { if (enabled == show) { return From 94678bd5ca7263efec0d7b4caea329b5605c3d25 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 02:13:57 +0200 Subject: [PATCH 06/20] Wait for idle scroll during key presses --- .../dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt index 9647b4dd..88ec49e8 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt @@ -64,8 +64,8 @@ class AccessibilityTest : DpadRecyclerViewTest() { fun testAccessibilityInfo() { repeat(10) { KeyEvents.pressDown() + waitForIdleScrollState() } - waitForIdleScrollState() initializeAccessibility() assertThat(accessibilityNodeInfo.actionList.find { From aaf0827f40e49351c6118b162f3486fccd5fc308 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 02:42:53 +0200 Subject: [PATCH 07/20] Decrease number of key events due to flakiness --- .../dpadrecyclerview/testing/KeyEventsTest.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt b/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt index 25bf1cee..28376d24 100644 --- a/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt +++ b/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt @@ -33,11 +33,14 @@ class KeyEventsTest : RecyclerViewTest() { launchGridFragment() Espresso.onIdle() - KeyEvents.pressRight(times = 10) - assert(DpadRecyclerViewAssertions.isFocused(position = 10)) + KeyEvents.pressRight(times = 8) + performActions(DpadRecyclerViewActions.waitForIdleScroll()) + + assert(DpadRecyclerViewAssertions.isFocused(position = 8)) + KeyEvents.pressLeft(times = 8) performActions(DpadRecyclerViewActions.waitForIdleScroll()) - KeyEvents.pressLeft(times = 10) + assert(DpadRecyclerViewAssertions.isFocused(position = 0)) } @@ -47,10 +50,13 @@ class KeyEventsTest : RecyclerViewTest() { Espresso.onIdle() KeyEvents.pressDown(times = 5) + performActions(DpadRecyclerViewActions.waitForIdleScroll()) + assert(DpadRecyclerViewAssertions.isFocused(position = 5 * 5)) - performActions(DpadRecyclerViewActions.waitForIdleScroll()) KeyEvents.pressUp(times = 5) + performActions(DpadRecyclerViewActions.waitForIdleScroll()) + assert(DpadRecyclerViewAssertions.isFocused(position = 0)) } } From c38d54aa24f1a68cc2496772d76627afd82569f2 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 09:32:07 +0200 Subject: [PATCH 08/20] Use Espresso action for key delays --- .../dpadrecyclerview/testing/KeyEventsTest.kt | 2 ++ .../dpadrecyclerview/testing/KeyEvents.kt | 34 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt b/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt index 28376d24..177bf534 100644 --- a/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt +++ b/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/KeyEventsTest.kt @@ -32,6 +32,7 @@ class KeyEventsTest : RecyclerViewTest() { fun testFastHorizontalScroll() { launchGridFragment() Espresso.onIdle() + performActions(DpadRecyclerViewActions.waitForAdapterUpdate()) KeyEvents.pressRight(times = 8) performActions(DpadRecyclerViewActions.waitForIdleScroll()) @@ -48,6 +49,7 @@ class KeyEventsTest : RecyclerViewTest() { fun testFastVerticalScroll() { launchGridFragment() Espresso.onIdle() + performActions(DpadRecyclerViewActions.waitForAdapterUpdate()) KeyEvents.pressDown(times = 5) performActions(DpadRecyclerViewActions.waitForIdleScroll()) diff --git a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt index 1c866bac..fa794595 100644 --- a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt +++ b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt @@ -16,10 +16,18 @@ package com.rubensousa.dpadrecyclerview.testing -import android.os.SystemClock import android.view.KeyEvent +import android.view.View +import androidx.test.espresso.Espresso +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice +import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf +import kotlin.math.max object KeyEvents { @@ -31,9 +39,9 @@ object KeyEvents { fun pressKey(key: Int, times: Int = 1, delay: Long = DEFAULT_KEY_PRESS_DELAY) { repeat(times) { device.pressKeyCode(key) - if (delay > 0) { - SystemClock.sleep(delay) - } + val actualDelay = max(25L, delay) + Espresso.onView(isRoot()).noActivity() + .perform(LoopMainThreadAction(actualDelay)) } } @@ -67,4 +75,22 @@ object KeyEvents { pressKey(KeyEvent.KEYCODE_DPAD_RIGHT, times, delay) } + private class LoopMainThreadAction( + private val delayMs: Long + ) : ViewAction { + + override fun getConstraints(): Matcher { + return allOf(isAssignableFrom(View::class.java)) + } + + override fun getDescription(): String { + return "Delaying main thread for $delayMs" + } + + override fun perform(uiController: UiController, view: View) { + uiController.loopMainThreadForAtLeast(delayMs) + } + + } + } From 859739bdd9e962a609b804236656911a7edb5257 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 09:37:32 +0200 Subject: [PATCH 09/20] Change order of layout to restore focus correctly --- .../dpadrecyclerview/test/TestNestedListFragment.kt | 2 +- .../test/tests/selection/SaveRestoreStateTest.kt | 2 ++ .../res/layout/dpadrecyclerview_test_container.xml | 10 +++++----- 3 files changed, 8 insertions(+), 6 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 57b366be..7ef37c17 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/TestNestedListFragment.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/TestNestedListFragment.kt @@ -116,7 +116,7 @@ class TestNestedListFragment : Fragment(R.layout.dpadrecyclerview_test_container onViewFocusedListener: OnViewFocusedListener, ) : RecyclerView.ViewHolder(view) { - val adapter = TestAdapter( + val adapter = TestAdapter( adapterConfiguration = configuration, onViewHolderSelected = { position -> }, onViewHolderDeselected = { position -> } 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 a1f1f452..b7f2c541 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 @@ -30,6 +30,7 @@ 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.helpers.waitForCondition +import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R import com.rubensousa.dpadrecyclerview.testing.assertions.DpadRecyclerViewAssertions @@ -56,6 +57,7 @@ class SaveRestoreStateTest { fun testSelectionStateIsSavedAndRestored() { KeyEvents.pressDown(times = 5) assertFocusPosition(5) + waitForIdleScrollState() fragmentScenario.recreate() diff --git a/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_test_container.xml b/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_test_container.xml index 792ef20b..2f148ce2 100644 --- a/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_test_container.xml +++ b/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_test_container.xml @@ -3,6 +3,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + - - From 746d19b211baaf07acf7f703f8ed71855e06c181 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 11:40:24 +0200 Subject: [PATCH 10/20] Use device.waitForIdle instead of LoopMainThreadAction --- .../dpadrecyclerview/testing/KeyEvents.kt | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt index fa794595..b289fdd9 100644 --- a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt +++ b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt @@ -17,16 +17,8 @@ package com.rubensousa.dpadrecyclerview.testing import android.view.KeyEvent -import android.view.View -import androidx.test.espresso.Espresso -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice -import org.hamcrest.Matcher -import org.hamcrest.Matchers.allOf import kotlin.math.max object KeyEvents { @@ -40,8 +32,7 @@ object KeyEvents { repeat(times) { device.pressKeyCode(key) val actualDelay = max(25L, delay) - Espresso.onView(isRoot()).noActivity() - .perform(LoopMainThreadAction(actualDelay)) + device.waitForIdle(actualDelay) } } @@ -75,22 +66,4 @@ object KeyEvents { pressKey(KeyEvent.KEYCODE_DPAD_RIGHT, times, delay) } - private class LoopMainThreadAction( - private val delayMs: Long - ) : ViewAction { - - override fun getConstraints(): Matcher { - return allOf(isAssignableFrom(View::class.java)) - } - - override fun getDescription(): String { - return "Delaying main thread for $delayMs" - } - - override fun perform(uiController: UiController, view: View) { - uiController.loopMainThreadForAtLeast(delayMs) - } - - } - } From 7bd67733f756d6b7b908593daff2b96b54b280f4 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 12:08:38 +0200 Subject: [PATCH 11/20] Revert back to SystemClock.sleep since waitForIdle does not work properly when disabling idle timeouts --- .../java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt index b289fdd9..9d8bbc5e 100644 --- a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt +++ b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt @@ -16,6 +16,7 @@ package com.rubensousa.dpadrecyclerview.testing +import android.os.SystemClock import android.view.KeyEvent import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -32,7 +33,7 @@ object KeyEvents { repeat(times) { device.pressKeyCode(key) val actualDelay = max(25L, delay) - device.waitForIdle(actualDelay) + SystemClock.sleep(actualDelay) } } From 46b47cd1085ed9c89c8c01ac603cd13fc73486e2 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 13:09:42 +0200 Subject: [PATCH 12/20] Fix focus not being retained in DpadRecyclerView when child count is 0 --- .../dpadrecyclerview/test/tests/focus/FocusListenerTest.kt | 6 +++--- .../com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 6629ad55..3e729677 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 @@ -23,7 +23,7 @@ import com.rubensousa.dpadrecyclerview.ChildAlignment import com.rubensousa.dpadrecyclerview.OnViewFocusedListener import com.rubensousa.dpadrecyclerview.ParentAlignment import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration -import com.rubensousa.dpadrecyclerview.test.helpers.assertIsNotFocused +import com.rubensousa.dpadrecyclerview.test.helpers.assertIsFocused import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition @@ -158,7 +158,7 @@ class FocusListenerTest : DpadRecyclerViewTest() { } @Test - fun testRecyclerViewLosesFocusWhenItLosesContent() { + fun testRecyclerViewKeepsFocusWhenItLosesContent() { // when executeOnFragment { fragment -> fragment.clearAdapter() @@ -168,7 +168,7 @@ class FocusListenerTest : DpadRecyclerViewTest() { } // then - assertIsNotFocused() + assertIsFocused() } } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt index c994cc49..43b88b91 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt @@ -399,7 +399,7 @@ open class DpadRecyclerView @JvmOverloads constructor( } private fun postRemoveView() { - if (isRetainingFocus && childCount > 0 && !hasFocus()) { + if (isRetainingFocus && !hasFocus()) { requestFocus() } isRetainingFocus = false From 420528be3df6430f989dd7aa85f5c2d90346e63b Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 17:50:57 +0200 Subject: [PATCH 13/20] Deflake tests --- .../test/tests/drag/DragHelperGridTest.kt | 41 ++++++++++++------- .../tests/layout/LayoutWhileScrollingTest.kt | 29 ------------- .../test/tests/scrolling/AccessibilityTest.kt | 6 +++ .../tests/touch/VerticalTouchScrollTest.kt | 1 + 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt index 7651e4bb..20fc56d7 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt @@ -28,6 +28,7 @@ import com.rubensousa.dpadrecyclerview.test.TestAdapterConfiguration import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusAndSelection import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView import com.rubensousa.dpadrecyclerview.test.helpers.waitForCondition +import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R import org.junit.Before @@ -35,7 +36,6 @@ import org.junit.Test class DragHelperGridTest { - private lateinit var fragmentScenario: FragmentScenario private lateinit var dragHelper: DpadDragHelper private lateinit var testAdapter: TestAdapter @@ -94,7 +94,10 @@ class DragHelperGridTest { startDragging(position = 0) // when - KeyEvents.pressRight(times = spanCount) + repeat(spanCount) { + KeyEvents.pressRight() + waitForIdleScrollState() + } // then assertFocusAndSelection(position = endRowPosition) @@ -114,7 +117,10 @@ class DragHelperGridTest { startDragging(position = endRowPosition) // when - KeyEvents.pressLeft(times = spanCount) + repeat(spanCount) { + KeyEvents.pressLeft() + waitForIdleScrollState() + } // then assertFocusAndSelection(position = 0) @@ -135,7 +141,7 @@ class DragHelperGridTest { startDragging(position = topColumnPosition) // when - KeyEvents.pressDown(times = 1) + KeyEvents.pressDown() // then assertFocusAndSelection(position = bottomColumnPosition) @@ -157,7 +163,7 @@ class DragHelperGridTest { startDragging(position = bottomColumnPosition) // when - KeyEvents.pressUp(times = 1) + KeyEvents.pressUp() // then assertFocusAndSelection(position = topColumnPosition) @@ -177,8 +183,13 @@ class DragHelperGridTest { startDragging(position = 0) // when - KeyEvents.pressDown(times = numberOfItems / spanCount) - KeyEvents.pressRight(times = spanCount) + repeat(numberOfItems / spanCount) { + KeyEvents.pressDown() + } + repeat(spanCount) { + KeyEvents.pressRight() + waitForIdleScrollState() + } // then assertFocusAndSelection(position = numberOfItems - 1) @@ -196,8 +207,14 @@ class DragHelperGridTest { startDragging(position = numberOfItems - 1) // when - KeyEvents.pressUp(times = numberOfItems / spanCount) - KeyEvents.pressLeft(times = spanCount) + repeat(numberOfItems / spanCount) { + KeyEvents.pressUp() + waitForIdleScrollState() + } + repeat(spanCount) { + KeyEvents.pressLeft() + waitForIdleScrollState() + } // then assertFocusAndSelection(position = 0) @@ -215,12 +232,6 @@ class DragHelperGridTest { } } - private fun stopDragging() { - InstrumentationRegistry.getInstrumentation().runOnMainSync { - dragHelper.stopDrag() - } - } - private fun launchFragment(): FragmentScenario { return launchFragmentInContainer( themeResId = R.style.DpadRecyclerViewTestTheme diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutWhileScrollingTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutWhileScrollingTest.kt index dfa1b299..7cd87329 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutWhileScrollingTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LayoutWhileScrollingTest.kt @@ -27,7 +27,6 @@ import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView import com.rubensousa.dpadrecyclerview.test.helpers.selectLastPosition import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest -import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule import org.junit.Before import org.junit.Rule @@ -89,34 +88,6 @@ class LayoutWhileScrollingTest : DpadRecyclerViewTest() { assertThat(layoutCompleted).isEqualTo(1) } - @Test - fun testRequestingLayoutDuringKeyEventsIsIgnored() { - // given - var layoutCompleted = 0 - onRecyclerView("Disable layout during scroll") { recyclerView -> - recyclerView.addOnLayoutCompletedListener( - object : DpadRecyclerView.OnLayoutCompletedListener { - override fun onLayoutCompleted(state: RecyclerView.State) { - layoutCompleted++ - } - }) - } - - // when - KeyEvents.pressDown(times = 20) - repeat(10) { - onRecyclerView("Request layout") { recyclerView -> - if (recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) { - recyclerView.requestLayout() - } - } - } - waitForIdleScrollState() - - // then - assertThat(layoutCompleted).isEqualTo(1) - } - @Test fun testRequestingLayoutDuringScrollIsNotIgnored() { // given diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt index 88ec49e8..389d242a 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/AccessibilityTest.kt @@ -16,6 +16,7 @@ package com.rubensousa.dpadrecyclerview.test.tests.scrolling +import android.os.Build import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat @@ -33,6 +34,7 @@ import com.rubensousa.dpadrecyclerview.testing.KeyEvents import org.junit.Before import org.junit.Test + class AccessibilityTest : DpadRecyclerViewTest() { private val spanCount = 5 @@ -62,6 +64,10 @@ class AccessibilityTest : DpadRecyclerViewTest() { @Test fun testAccessibilityInfo() { + // Not working on these APIs, so just exit + if (Build.VERSION.SDK_INT <= 25) { + return + } repeat(10) { KeyEvents.pressDown() waitForIdleScrollState() diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/touch/VerticalTouchScrollTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/touch/VerticalTouchScrollTest.kt index 82b4f624..5f8ce862 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/touch/VerticalTouchScrollTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/touch/VerticalTouchScrollTest.kt @@ -81,6 +81,7 @@ class VerticalTouchScrollTest : DpadRecyclerViewTest() { val verticalScrollOffset = firstItemBounds.height() / 2 swipeVerticallyBy(-verticalScrollOffset) + waitForIdleScrollState() val newFirstItemBounds = getItemViewBounds(position = 0) assertThat(newFirstItemBounds.top).isLessThan(firstItemBounds.top) From 69e9d2c9863126154a07e1ac6b67e4135325a033 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 22:49:27 +0200 Subject: [PATCH 14/20] Do not add delay for single key press --- .../com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt index 9d8bbc5e..2f8e09af 100644 --- a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt +++ b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/KeyEvents.kt @@ -32,8 +32,10 @@ object KeyEvents { fun pressKey(key: Int, times: Int = 1, delay: Long = DEFAULT_KEY_PRESS_DELAY) { repeat(times) { device.pressKeyCode(key) - val actualDelay = max(25L, delay) - SystemClock.sleep(actualDelay) + if (times > 1) { + val actualDelay = max(25L, delay) + SystemClock.sleep(actualDelay) + } } } From e0ad5d8ef52198d9276cd487631fd5dd20114b36 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 22:49:36 +0200 Subject: [PATCH 15/20] Deflake looping layout tests --- .../test/tests/layout/LoopingLayoutTest.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt index 20d2010e..970460e3 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt @@ -98,8 +98,10 @@ class LoopingLayoutTest : DpadRecyclerViewTest() { fun testLayoutScrollsBackwardsToStartPosition() { launchFragment(getDefaultAdapterConfiguration().copy(numberOfItems = 10)) - KeyEvents.pressLeft(times = 10) - waitForIdleScrollState() + repeat(10) { + KeyEvents.pressLeft() + waitForIdleScrollState() + } val currentBounds = getItemViewBounds(position = 0) assertFocusAndSelection(position = 0) @@ -110,8 +112,10 @@ class LoopingLayoutTest : DpadRecyclerViewTest() { fun testLayoutScrollsForwardsToStartPosition() { launchFragment(getDefaultAdapterConfiguration().copy(numberOfItems = 10)) - KeyEvents.pressRight(times = 10) - waitForIdleScrollState() + repeat(10) { + KeyEvents.pressRight() + waitForIdleScrollState() + } val currentBounds = getItemViewBounds(position = 0) assertFocusAndSelection(position = 0) @@ -144,14 +148,20 @@ class LoopingLayoutTest : DpadRecyclerViewTest() { } // Scroll forwards to begin the loop - KeyEvents.pressRight(times = 10) + repeat(10) { + KeyEvents.pressRight() + waitForIdleScrollState() + } assertFocusAndSelection(position = 0) assertThat(getItemViewBounds(position = 0).centerX()) .isEqualTo(getRecyclerViewBounds().centerX()) // Scroll backwards and validate start loop doesn't exist - KeyEvents.pressLeft(times = 15) + repeat(15) { + KeyEvents.pressLeft() + waitForIdleScrollState() + } assertFocusAndSelection(position = 0) assertThat(getChildrenBounds()).isEqualTo(startChildrenBounds) } From 03e1a2972ebce60b771838ee986257d19f7045b9 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 23:07:30 +0200 Subject: [PATCH 16/20] Add missing wait for idle scroll --- .../dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt index 20fc56d7..87437920 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/drag/DragHelperGridTest.kt @@ -185,6 +185,7 @@ class DragHelperGridTest { // when repeat(numberOfItems / spanCount) { KeyEvents.pressDown() + waitForIdleScrollState() } repeat(spanCount) { KeyEvents.pressRight() From e95db210f27d9fd13379c4177b6d0eb10a49a69b Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 23:21:40 +0200 Subject: [PATCH 17/20] Deflake grid vertical scroll test --- .../test/tests/scrolling/VerticalGridScrollTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/VerticalGridScrollTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/VerticalGridScrollTest.kt index b763a829..8acbbdfe 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/VerticalGridScrollTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/VerticalGridScrollTest.kt @@ -294,13 +294,13 @@ class VerticalGridScrollTest : DpadRecyclerViewTest() { // Scroll down until last uneven child is visible repeat(5) { - KeyEvents.pressDown(delay = 500L) + KeyEvents.pressDown() + waitForIdleScrollState() } - waitForIdleScrollState() - repeat(5) { - KeyEvents.pressUp(delay = 500L) + KeyEvents.pressUp() + waitForIdleScrollState() } assertThat(getChildrenBounds()).isEqualTo(childPositions) From 63e0f97dcd5faa2a0a9eb95d825010d514c18767 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 23:22:48 +0200 Subject: [PATCH 18/20] Remove screen recorder rule for small modules since it just slows down tests --- dpadrecyclerview-compose/build.gradle | 1 - .../compose/DpadComposeFocusViewHolderTest.kt | 4 ---- .../dpadrecyclerview/compose/DpadComposeViewHolderTest.kt | 4 ---- dpadrecyclerview-testing/build.gradle | 1 - .../rubensousa/dpadrecyclerview/testing/RecyclerViewTest.kt | 5 ----- 5 files changed, 15 deletions(-) diff --git a/dpadrecyclerview-compose/build.gradle b/dpadrecyclerview-compose/build.gradle index 139993c0..6a198a00 100644 --- a/dpadrecyclerview-compose/build.gradle +++ b/dpadrecyclerview-compose/build.gradle @@ -18,7 +18,6 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments useTestStorageService: 'true' testInstrumentationRunnerArguments additionalTestOutputDir: 'storage/emulated/0/recordings/com.rubensousa.dpadrecyclerview.compose.test' - testInstrumentationRunnerArguments listener: 'com.rubensousa.dpadrecyclerview.testfixtures.recording.TestRecordingListener' } buildTypes { diff --git a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt index 03a1b605..2c29a141 100644 --- a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt +++ b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolderTest.kt @@ -32,7 +32,6 @@ import com.rubensousa.dpadrecyclerview.DpadRecyclerView import com.rubensousa.dpadrecyclerview.ExtraLayoutSpaceStrategy import com.rubensousa.dpadrecyclerview.compose.test.ComposeFocusTestActivity import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent -import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions @@ -43,9 +42,6 @@ import org.junit.Test class DpadComposeFocusViewHolderTest { - @get:Rule - val screenRecorderRule = ScreenRecorderRule() - @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt index f801d6f6..2ed41239 100644 --- a/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt +++ b/dpadrecyclerview-compose/src/androidTest/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolderTest.kt @@ -30,7 +30,6 @@ import com.rubensousa.dpadrecyclerview.DpadRecyclerView import com.rubensousa.dpadrecyclerview.ExtraLayoutSpaceStrategy import com.rubensousa.dpadrecyclerview.compose.test.ViewFocusTestActivity import com.rubensousa.dpadrecyclerview.testfixtures.DpadFocusEvent -import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions @@ -40,9 +39,6 @@ import org.junit.Test class DpadComposeViewHolderTest { - @get:Rule - val screenRecorderRule = ScreenRecorderRule() - @get:Rule val composeTestRule = createAndroidComposeRule() diff --git a/dpadrecyclerview-testing/build.gradle b/dpadrecyclerview-testing/build.gradle index 1700baa5..db3ca459 100644 --- a/dpadrecyclerview-testing/build.gradle +++ b/dpadrecyclerview-testing/build.gradle @@ -17,7 +17,6 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments useTestStorageService: 'true' testInstrumentationRunnerArguments additionalTestOutputDir: 'storage/emulated/0/recordings/com.rubensousa.dpadrecyclerview.testing.test' - testInstrumentationRunnerArguments listener: 'com.rubensousa.dpadrecyclerview.testfixtures.recording.TestRecordingListener' multiDexEnabled true } diff --git a/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/RecyclerViewTest.kt b/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/RecyclerViewTest.kt index 7df68b9e..c781f5b2 100644 --- a/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/RecyclerViewTest.kt +++ b/dpadrecyclerview-testing/src/androidTest/java/com/rubensousa/dpadrecyclerview/testing/RecyclerViewTest.kt @@ -24,17 +24,12 @@ import androidx.test.espresso.ViewAssertion import androidx.test.espresso.ViewInteraction import androidx.test.espresso.matcher.ViewMatchers import com.google.common.truth.Truth.assertThat -import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule -import org.junit.Rule abstract class RecyclerViewTest { private lateinit var subPositionFragment: FragmentScenario private lateinit var gridFragment: FragmentScenario - @get:Rule - val screenRecordingRule = ScreenRecorderRule() - protected fun onGridFragment(block: (fragment: DpadGridFragment) -> Unit) { gridFragment.onFragment { fragment -> block(fragment) From 7ac6df05cbdea3ebc4a754db919036bdda77083f Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 19 Jun 2024 23:25:46 +0200 Subject: [PATCH 19/20] Remove fast key presses in looping layout tests to deflake them in lower end devices --- .../dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt index 970460e3..2cbe0948 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/LoopingLayoutTest.kt @@ -17,17 +17,12 @@ import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R -import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule -import org.junit.Rule import org.junit.Test import kotlin.math.ceil import kotlin.math.min class LoopingLayoutTest : DpadRecyclerViewTest() { - @get:Rule - val idleTimeoutRule = DisableIdleTimeoutRule() - override fun getDefaultAdapterConfiguration(): TestAdapterConfiguration { return super.getDefaultAdapterConfiguration() .copy(itemLayoutId = R.layout.dpadrecyclerview_test_item_horizontal) From c0ef4711b201a02007e6cb82fa86468d35be17fa Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Thu, 20 Jun 2024 00:08:21 +0200 Subject: [PATCH 20/20] Exclude lower APIs due to emulator instability --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a7f4c855..d218ac95 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -49,7 +49,7 @@ jobs: matrix: arch: [ x86 ] target: [ android-tv ] - api-level: [22, 25, 27, 28, 29, 30, 31] + api-level: [27, 28, 29, 30, 31] profile: [tv_1080p] steps: - name: checkout