Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send focus to composables now that view-interop is fixed #195

Merged
merged 27 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
98bc8bb
Send focus to composables now that view-interop is fixed
rubensousa Mar 13, 2024
1e713c1
Merge branch 'version_catalog' of https://github.com/rubensousa/DpadR…
rubensousa Mar 13, 2024
24a0b8f
Allow postponing layout requests during scrolling events
rubensousa Mar 14, 2024
3e1db05
Add ViewHolder that sends focus down to composables
rubensousa Mar 14, 2024
69bfea7
Merge branch 'master' of https://github.com/rubensousa/DpadRecyclerVi…
rubensousa Mar 14, 2024
7bd009b
Merge branch 'master' of https://github.com/rubensousa/DpadRecyclerVi…
rubensousa Mar 14, 2024
faf7fe7
Rename enableLayoutChangesWhileScrolling to setLayoutWhileScrollingEn…
rubensousa Mar 14, 2024
3ded2de
Remove unnecessary guava configuration
rubensousa Mar 14, 2024
4560fe6
Add UI tests for layout while scrolling
rubensousa Mar 14, 2024
1ebc837
Add missing api change
rubensousa Mar 14, 2024
478c0a7
Delete DpadAbstractComposeViewHolder
rubensousa Mar 14, 2024
a86d7fb
Collect screen recordings for UI tests
rubensousa Mar 14, 2024
5ceee81
Uninstall previous test apks
rubensousa Mar 15, 2024
e7739ef
Move uninstall all to emulator job
rubensousa Mar 15, 2024
5f2a290
Add --info to see test output
rubensousa Mar 15, 2024
436b3d4
Wait until scroll state changes to idle to request layout
rubensousa Mar 15, 2024
44a4e1d
Attempt to decrease test flakiness
rubensousa Mar 15, 2024
c37052d
Don't request layout if recyclerview is already idle
rubensousa Mar 15, 2024
39db07f
Decrease number of layout requests
rubensousa Mar 15, 2024
c7476e6
Add screen recorder rule to collect failure reports
rubensousa Mar 15, 2024
4eeb075
New attempt to deflake tests
rubensousa Mar 15, 2024
4642a37
Decrease number of items in layout
rubensousa Mar 15, 2024
b414a1f
Add order in UI test execution
rubensousa Mar 15, 2024
89e54ca
Fix sample compose test
rubensousa Mar 16, 2024
708989c
Clean-up public API of compose extension
rubensousa Mar 16, 2024
d567215
New attempt to deflake test
rubensousa Mar 16, 2024
3f7e477
Fix kdoc
rubensousa Mar 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ jobs:
ram-size: 4096M
emulator-options: -no-window -no-snapshot-save -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: |
./scripts/install_test_apks.sh
./scripts/run_instrumented_tests.sh dpadrecyclerview
./scripts/run_instrumented_tests.sh dpadrecyclerview-testing
./scripts/run_instrumented_tests.sh dpadrecyclerview-compose
./scripts/run_instrumented_tests.sh sample
./gradlew uninstallAll
./gradlew --build-cache dpadrecyclerview-compose:connectedDebugAndroidTest
./gradlew --build-cache sample:connectedDebugAndroidTest
./gradlew --build-cache dpadrecyclerview-testing:connectedDebugAndroidTest
./gradlew --build-cache dpadrecyclerview:connectedDebugAndroidTest --info

- name: Upload artifacts
uses: actions/upload-artifact@v3
Expand Down
21 changes: 14 additions & 7 deletions dpadrecyclerview-compose/api/dpadrecyclerview-compose.api
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
public abstract class com/rubensousa/dpadrecyclerview/compose/DpadAbstractComposeViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder, com/rubensousa/dpadrecyclerview/DpadViewHolder {
public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeExtensionsKt {
public static final fun dpadClickable (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/Modifier;
}

public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder, com/rubensousa/dpadrecyclerview/DpadViewHolder {
public static final field $stable I
public fun <init> (Landroid/view/ViewGroup;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;)V
public synthetic fun <init> (Landroid/view/ViewGroup;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public abstract fun Content (Ljava/lang/Object;ZZLandroidx/compose/runtime/Composer;I)V
public fun <init> (Landroid/view/ViewGroup;Landroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function4;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Landroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getItem ()Ljava/lang/Object;
public fun getSubPositionAlignments ()Ljava/util/List;
public fun onFocusChanged (Z)V
public fun onViewHolderDeselected ()V
public fun onViewHolderSelected ()V
public fun onViewHolderSelectedAndAligned ()V
public final fun setItemState (Ljava/lang/Object;)V
}

public class com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolder : com/rubensousa/dpadrecyclerview/compose/DpadAbstractComposeViewHolder {
public final class com/rubensousa/dpadrecyclerview/compose/DpadComposeViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder, com/rubensousa/dpadrecyclerview/DpadViewHolder {
public static final field $stable I
public fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function5;)V
public synthetic fun <init> (Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/platform/ViewCompositionStrategy;Lkotlin/jvm/functions/Function5;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun Content (Ljava/lang/Object;ZZLandroidx/compose/runtime/Composer;I)V
public final fun getItem ()Ljava/lang/Object;
public fun getSubPositionAlignments ()Ljava/util/List;
public fun onViewHolderDeselected ()V
public fun onViewHolderSelected ()V
public fun onViewHolderSelectedAndAligned ()V
public final fun setItemState (Ljava/lang/Object;)V
}

public final class com/rubensousa/dpadrecyclerview/compose/RecyclerViewCompositionStrategy {
Expand Down
5 changes: 3 additions & 2 deletions dpadrecyclerview-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ android {
targetSdk versions.targetSdkVersion
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'
}

Expand Down Expand Up @@ -57,14 +58,14 @@ dependencies {
implementation libs.androidx.appcompat
implementation libs.androidx.recyclerview
implementation libs.androidx.customview.poolingcontainer
implementation libs.androidx.compose.foundation
implementation libs.androidx.compose.ui
implementation libs.androidx.compose.ui.tooling.preview

// Test dependencies
debugImplementation libs.androidx.test.compose.ui.manifest
debugImplementation libs.androidx.compose.ui.tooling
debugImplementation libs.androidx.compose.ui.tooling.preview
debugImplementation libs.androidx.compose.material3
debugImplementation libs.androidx.customview
androidTestImplementation project(':dpadrecyclerview-testing')
androidTestImplementation project(':dpadrecyclerview-test-fixtures')
androidTestImplementation libs.androidx.test.compose.ui.junit4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright 2023 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.compose

import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.ExtraLayoutSpaceStrategy
import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions
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()

@get:Rule
val composeTestRule = createAndroidComposeRule<ComposeFocusTestActivity>()

@Test
fun testComposeItemsReceiveFocus() {
assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)

KeyEvents.pressDown()
waitForIdleScroll()

assertFocus(item = 1, isFocused = true)
assertSelection(item = 1, isSelected = true)
}

@Test
fun testComposeFocusChanges() {
composeTestRule.activityRule.scenario.onActivity { activity ->
activity.clearFocus()
}

Espresso.onIdle()
assertFocus(item = 0, isFocused = false)
assertSelection(item = 0, isSelected = true)

composeTestRule.activityRule.scenario.onActivity { activity ->
activity.requestFocus()
}

assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)
}

@Test
fun testClicksAreDispatched() {
// given
var clicks: List<Int> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
clicks = activity.getClicks()
}

// when
KeyEvents.click()

// then
assertThat(clicks).isEqualTo(listOf(0))
}

@Test
fun testCompositionIsClearedWhenClearingAdapter() {
val viewHolders = ArrayList<RecyclerView.ViewHolder>()
composeTestRule.activityRule.scenario.onActivity { activity ->
viewHolders.addAll(activity.getViewsHolders())
activity.removeAdapter()
}

viewHolders.forEach { viewHolder ->
val composeView = viewHolder.itemView as DpadComposeView
assertThat(composeView.hasComposition()).isFalse()
}
composeTestRule.onNodeWithText("0").assertDoesNotExist()
}

@Test
fun testCompositionIsNotClearedWhenDetachingFromWindow() {
composeTestRule.activityRule.scenario.onActivity { activity ->
activity.getRecyclerView().setExtraLayoutSpaceStrategy(object : ExtraLayoutSpaceStrategy {
override fun calculateStartExtraLayoutSpace(state: RecyclerView.State): Int {
return 1080
}
})
}
repeat(3) {
KeyEvents.pressDown()
waitForIdleScroll()
}

composeTestRule.onNodeWithText("0").assertExists()
composeTestRule.onNodeWithText("0").assertIsNotDisplayed()
}

@Test
fun testCompositionIsClearedWhenViewHolderIsRecycled() {
repeat(10) {
KeyEvents.pressDown()
waitForIdleScroll()
}

composeTestRule.onNodeWithText("0").assertDoesNotExist()

var disposals: List<Int> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
disposals = activity.getDisposals()
}

assertThat(disposals).contains(0)
}

private fun waitForIdleScroll() {
onView(ViewMatchers.isAssignableFrom(DpadRecyclerView::class.java))
.perform(DpadRecyclerViewActions.waitForIdleScroll())
}

private fun assertFocus(item: Int, isFocused: Boolean) {
composeTestRule.onNodeWithText(item.toString()).assertIsDisplayed()
.assert(SemanticsMatcher.expectValue(TestComposable.focusedKey, isFocused))
}

private fun assertSelection(item: Int, isSelected: Boolean) {
composeTestRule.onNodeWithText(item.toString()).assertIsDisplayed()
.assert(SemanticsMatcher.expectValue(TestComposable.selectedKey, isSelected))
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Rúben Sousa
* 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.
Expand All @@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.rubensousa.dpadrecyclerview.compose

import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
Expand All @@ -30,33 +28,32 @@ import androidx.test.espresso.matcher.ViewMatchers
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
import com.rubensousa.dpadrecyclerview.ExtraLayoutSpaceStrategy
import com.rubensousa.dpadrecyclerview.testfixtures.recording.ScreenRecorderRule
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.actions.DpadRecyclerViewActions
import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule
import org.junit.Rule
import org.junit.Test

class DpadComposeViewHolderTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<TestActivity>()
val idleTimeoutRule = DisableIdleTimeoutRule()

@get:Rule
val screenRecorderRule = ScreenRecorderRule()

@get:Rule
val composeTestRule = createAndroidComposeRule<ViewFocusTestActivity>()

@Test
fun testComposeItemsReceiveFocus() {
assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)

assertFocus(item = 1, isFocused = false)
assertSelection(item = 1, isSelected = false)

assertFocus(item = 2, isFocused = false)
assertSelection(item = 2, isSelected = false)

KeyEvents.pressDown()
waitForIdleScroll()

assertFocus(item = 0, isFocused = false)
assertSelection(item = 0, isSelected = false)

assertFocus(item = 1, isFocused = true)
assertSelection(item = 1, isSelected = true)
}
Expand All @@ -71,24 +68,12 @@ class DpadComposeViewHolderTest {
assertFocus(item = 0, isFocused = false)
assertSelection(item = 0, isSelected = true)

assertFocus(item = 1, isFocused = false)
assertSelection(item = 1, isSelected = false)

assertFocus(item = 2, isFocused = false)
assertSelection(item = 2, isSelected = false)

composeTestRule.activityRule.scenario.onActivity { activity ->
activity.requestFocus()
}

assertFocus(item = 0, isFocused = true)
assertSelection(item = 0, isSelected = true)

assertFocus(item = 1, isFocused = false)
assertSelection(item = 1, isSelected = false)

assertFocus(item = 2, isFocused = false)
assertSelection(item = 2, isSelected = false)
}

@Test
Expand All @@ -102,19 +87,17 @@ class DpadComposeViewHolderTest {

@Test
fun testClicksAreDispatched() {
KeyEvents.click()

KeyEvents.pressDown()
waitForIdleScroll()

KeyEvents.click()

// given
var clicks: List<Int> = emptyList()
composeTestRule.activityRule.scenario.onActivity { activity ->
clicks = activity.getClicks()
}

assertThat(clicks).isEqualTo(listOf(0, 1))
// when
KeyEvents.click()

// then
assertThat(clicks).isEqualTo(listOf(0))
}

@Test
Expand All @@ -126,8 +109,8 @@ class DpadComposeViewHolderTest {
}

viewHolders.forEach { viewHolder ->
val composeView = viewHolder.itemView as ComposeView
assertThat(composeView.hasComposition).isFalse()
val composeView = viewHolder.itemView as DpadComposeView
assertThat(composeView.hasComposition()).isFalse()
}
composeTestRule.onNodeWithText("0").assertDoesNotExist()
}
Expand Down Expand Up @@ -183,5 +166,3 @@ class DpadComposeViewHolderTest {
}

}


11 changes: 10 additions & 1 deletion dpadrecyclerview-compose/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

<application tools:ignore="MissingTvBanner">
<activity
android:name=".TestActivity"
android:name=".ComposeFocusTestActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true">
<intent-filter>
Expand All @@ -37,6 +37,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ViewFocusTestActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Loading
Loading