diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleEntryBackTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleEntryBackTest.kt new file mode 100644 index 00000000..5f5d22dc --- /dev/null +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleEntryBackTest.kt @@ -0,0 +1,107 @@ +package org.justinvisser.wolfskin.tv + +import android.app.Application +import android.os.Looper +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.ui.nav.Destination +import io.mockk.coEvery +import io.mockk.mockk +import java.util.UUID +import kotlinx.coroutines.cancel +import org.junit.After +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.justinvisser.wolfskin.conductarr.MemoryCacheStore +import org.justinvisser.wolfskin.design.* +import org.justinvisser.wolfskin.tv.read.PersonRead +import org.justinvisser.wolfskin.tv.read.TitleReadFacade +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows +import org.robolectric.annotation.Config + +/** A loaded outgoing destination remains composed after pop but cannot consume the incoming Home Back. */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") +class TitleEntryBackTest { + @get:Rule val rule = createAndroidComposeRule() + private lateinit var viewModel: WolfskinTitleViewModel + private lateinit var conductarr: ConductarrSession + + @After fun dispose() { + if (::viewModel.isInitialized) viewModel.viewModelScope.cancel() + if (::conductarr.isInitialized) conductarr.close() + } + + @Test fun outgoingLoadedTitleCannotConsumeTheNextHomeBack() = verifyOutgoingBack(personPage = false) + @Test fun outgoingLoadedPersonCannotConsumeTheNextHomeBack() = verifyOutgoingBack(personPage = true) + + private fun verifyOutgoingBack(personPage: Boolean) { + val activity = rule.activity + val navigation = NavigationManager() + val viewer = ShellPerson("justin", "Justin", "J", false) + val shell = ShellState(listOf(viewer), viewer.key) { + if (it == ShellAction.Exit) navigation.requestExit() + }.apply { pickerVisible = false; pickerLaunch = false; navFocused = false } + val id = UUID.fromString("00000000-0000-0000-0000-000000000031") + val route = if (personPage) personRoute(id.toString()) else titleRoute(id.toString()) + val destination = Destination.Wolfskin(route) + val facade = mockk() + if (personPage) { + coEvery { facade.readPersonSaved(id) } returns null + coEvery { facade.readPerson(id, 0) } returns PersonRead(TitlePerson(id.toString(), "Outgoing actor"), + listOf(TitleItem("credit", "Credit", "movie"))) + } else { + coEvery { facade.readSaved(id, any()) } returns null + coEvery { facade.read(id, any(), any()) } returns TitlePageData(TitleItem(id.toString(), "Outgoing film", "movie", held = true)) + } + conductarr = ConductarrSession(MemoryCacheStore()) + viewModel = WolfskinTitleViewModel(activity, facade, mockk(), navigation, conductarr, + mockk(relaxed = true)) + shell.enterDestination(route) + navigation.navigateTo(destination) + val home = HomeSnapshot(listOf(HomeRow("hero", HomeRowKind.Hero, cards = listOf(HomeTitle("home-film", "Home film", "movie"))))) + val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null) + rule.setContent { + WolfskinTheme { + rememberWolfskinBackHandler(shell, navigation) + val current = navigation.backStack.last() + LaunchedEffect(current) { if (current is Destination.Home) shell.enterDestination("home") } + Box { + ScreenEntry(shell, "home", current is Destination.Home) { HomeScreen(shell, home) {} } + // Retain the real outgoing destination just as NavDisplay does during its exit animation. + ScreenEntry(shell, route, current == destination) { + TitleDestination(route, shell, preferences, viewModel = viewModel, fixtures = false) + } + } + } + } + rule.waitUntil(5_000) { + Snapshot.sendApplyNotifications() + Shadows.shadowOf(Looper.getMainLooper()).idle() + (if (personPage) viewModel.person != null else viewModel.title?.data?.status == TitleReadStatus.Ready) && !shell.navFocused + } + rule.runOnIdle { activity.onBackPressedDispatcher.onBackPressed() } + rule.waitForIdle() + assertEquals(listOf(Destination.Home()), navigation.backStack.toList()) + assertFalse(shell.navFocused) + rule.runOnIdle { activity.onBackPressedDispatcher.onBackPressed() } + rule.mainClock.advanceTimeBy(16) + assertTrue("The incoming Home must receive Back while the outgoing entry is retained", shell.navFocused) + assertFalse("A second Back must not finish the task through the outgoing entry", activity.isFinishing) + assertEquals(listOf(Destination.Home()), navigation.backStack.toList()) + rule.runOnIdle { activity.onBackPressedDispatcher.onBackPressed() } + rule.mainClock.advanceTimeBy(16) + assertTrue("Only the following Back from Home nav should exit", activity.isFinishing) + } +}