From fa4a3c841b60317d0571f6cd477b0fe943052b9d Mon Sep 17 00:00:00 2001 From: sebaslogen Date: Tue, 16 Jul 2024 17:24:41 +0200 Subject: [PATCH] Add automated test for keys in scope bugfix --- .../sample/ComposeActivityRecreationTests.kt | 5 +- .../KeysInScopeActivityRecreationTests.kt | 59 +++++++++++++++++++ .../sample/utils/ComposeTestUtils.kt | 7 --- ...ScreenWithSingleViewModelScopedWithKeys.kt | 3 +- 4 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/KeysInScopeActivityRecreationTests.kt diff --git a/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/ComposeActivityRecreationTests.kt b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/ComposeActivityRecreationTests.kt index 0cd599e4..c1023bec 100644 --- a/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/ComposeActivityRecreationTests.kt +++ b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/ComposeActivityRecreationTests.kt @@ -28,6 +28,7 @@ class ComposeActivityRecreationTests : ComposeTestUtils { @Before fun setUp() { + showSingleScopedViewModel = null scenario = ActivityScenario.launch( Intent(ApplicationProvider.getApplicationContext(), ComposeActivity::class.java).apply { putExtra(ComposeActivity.START_DESTINATION, viewModelScopedDestination) @@ -35,7 +36,7 @@ class ComposeActivityRecreationTests : ComposeTestUtils { } @Test - fun whenISwitchFromLightModeToNightMode_thenTheOneAndOnlyScopedViewModelThatSOnlyUsedInLightModeIsGone() { + fun whenActivityRecreates_thenTheOneAndOnlyScopedViewModelThatSOnlyUsedInLightModeIsGone() { // Given the starting screen with ViewModel scoped that is ONLY shown in light mode composeTestRule.waitForIdle() // Find the scoped text fields and grab their texts @@ -54,7 +55,7 @@ class ComposeActivityRecreationTests : ComposeTestUtils { onNodeWithTestTag("FakeInjectedViewModel Scoped", assertDisplayed = false).assertDoesNotExist() assert(finalAmountOfViewModelsCleared == initialAmountOfViewModelsCleared + 1) { "The amount of FakeInjectedViewModel that were cleared after key change ($finalAmountOfViewModelsCleared) " + - "was not higher that the amount before the key change ($initialAmountOfViewModelsCleared)" + "was not higher that the amount before the key change ($initialAmountOfViewModelsCleared)" } } } \ No newline at end of file diff --git a/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/KeysInScopeActivityRecreationTests.kt b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/KeysInScopeActivityRecreationTests.kt new file mode 100644 index 00000000..c7787174 --- /dev/null +++ b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/KeysInScopeActivityRecreationTests.kt @@ -0,0 +1,59 @@ +package com.sebaslogen.resacaapp.sample + +import android.content.Intent +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performScrollToIndex +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.sebaslogen.resaca.COMPOSITION_RESUMED_TIMEOUT_IN_SECONDS +import com.sebaslogen.resacaapp.sample.ui.main.ComposeActivity +import com.sebaslogen.resacaapp.sample.ui.main.viewModelScopedWithKeysDestination +import com.sebaslogen.resacaapp.sample.utils.ComposeTestUtils +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +@LargeTest +class KeysInScopeActivityRecreationTests : ComposeTestUtils { + + private lateinit var scenario: ActivityScenario + + @get:Rule + override val composeTestRule = createEmptyComposeRule() + + @Before + fun setUp() { + scenario = ActivityScenario.launch( + Intent(ApplicationProvider.getApplicationContext(), ComposeActivity::class.java).apply { + putExtra(ComposeActivity.START_DESTINATION, viewModelScopedWithKeysDestination) + }) + } + + @Test + fun givenAListOfKeysAndALazyListOfScopedViewModelsScrolledToTheMiddle_whenActivityRecreatesAndIScrollBackToTop_thenTheOriginalScopedViewModelIsStillPresent() { + // Given the starting screen with ViewModels scoped + composeTestRule.waitForIdle() + // Find the scoped text fields and grab their texts + val initialFakeScopedViewModelText = retrieveTextFromNodeWithTestTag("FakeScopedViewModel 1 Scoped") + printComposeUiTreeToLog() + // Scroll away from first item + composeTestRule.onNodeWithTag("LazyList").performScrollToIndex(50) + + // When we trigger a configuration change by recreating the Activity and scroll back to the top + scenario.recreate() + printComposeUiTreeToLog() + Thread.sleep(COMPOSITION_RESUMED_TIMEOUT_IN_SECONDS * 1000) // Wait for the ViewModel to be cleared + composeTestRule.onNodeWithTag("LazyList").performScrollToIndex(0) + + // Then the scoped ViewModel disappears + onNodeWithTestTag("FakeScopedViewModel 1 Scoped").assertIsDisplayed().assertTextEquals(initialFakeScopedViewModelText) + } +} \ No newline at end of file diff --git a/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt index 49b6459c..0a7d3438 100644 --- a/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt +++ b/sample/src/androidTest/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt @@ -1,25 +1,18 @@ package com.sebaslogen.resacaapp.sample.utils -import android.content.Intent import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.printToLog import androidx.compose.ui.text.AnnotatedString -import androidx.test.core.app.ActivityScenario -import androidx.test.core.app.ApplicationProvider -import com.sebaslogen.resacaapp.sample.ui.main.ComposeActivity -import com.sebaslogen.resacaapp.sample.ui.main.koinViewModelScopedDestination import com.sebaslogen.resacaapp.sample.ui.main.showSingleScopedViewModel import org.junit.After import org.junit.Before -import org.koin.core.context.stopKoin interface ComposeTestUtils { diff --git a/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/compose/screens/ComposeScreenWithSingleViewModelScopedWithKeys.kt b/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/compose/screens/ComposeScreenWithSingleViewModelScopedWithKeys.kt index c4fcf6c5..849336a9 100644 --- a/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/compose/screens/ComposeScreenWithSingleViewModelScopedWithKeys.kt +++ b/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/compose/screens/ComposeScreenWithSingleViewModelScopedWithKeys.kt @@ -10,6 +10,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.sebaslogen.resaca.rememberKeysInScope @@ -34,7 +35,7 @@ fun ComposeScreenWithSingleViewModelScopedWithKeys(navController: NavHostControl text = "The list below contains one ViewModel per row that will stay in memory due to KeysInScope as long as the list and screen are displayed" ) val keys = rememberKeysInScope(inputListOfKeys = listItems) - LazyColumn(modifier = Modifier.fillMaxHeight()) { + LazyColumn(modifier = Modifier.fillMaxHeight().testTag("LazyList")) { items(items = listItems, key = { it.number }) { item -> val fakeScopedVM: FakeScopedViewModel = viewModelScoped(key = item, keyInScopeResolver = keys) DemoComposable(inputObject = fakeScopedVM, objectType = "FakeScopedViewModel $item", scoped = true)