Reconcile removed Anime roots without discarding pushed pages
All checks were successful
review/code-gpt GPT review blocker B1 (Anime root removal strands Back) fixed: delta read by firstmate (ShellState reconciles route/root to Home, preservePushed replaces stack root, 111-line regression, 16 targeted tests green); all other review areas cleared on 9e15dbee
All checks were successful
review/code-gpt GPT review blocker B1 (Anime root removal strands Back) fixed: delta read by firstmate (ShellState reconciles route/root to Home, preservePushed replaces stack root, 111-line regression, 16 targeted tests green); all other review areas cleared on 9e15dbee
This commit is contained in:
parent
9e15dbee8c
commit
6e0f9952cf
4 changed files with 133 additions and 5 deletions
|
|
@ -0,0 +1,111 @@
|
|||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
|
||||
import org.justinvisser.wolfskin.design.ShellKey
|
||||
import org.justinvisser.wolfskin.tv.read.LibraryReadFacade
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class)
|
||||
class AnimeRootRemovalTest {
|
||||
private var previousFixtures = false
|
||||
private var previousUrl = ""
|
||||
|
||||
@Before fun setUp() {
|
||||
Dispatchers.setMain(Dispatchers.Unconfined)
|
||||
ConductarrPreference.initialize(ApplicationProvider.getApplicationContext<Application>())
|
||||
previousFixtures = ConductarrPreference.fixtures
|
||||
previousUrl = ConductarrPreference.baseUrl
|
||||
ConductarrPreference.updateFixtures(false)
|
||||
}
|
||||
|
||||
@After fun tearDown() {
|
||||
ConductarrPreference.updateBaseUrl(previousUrl)
|
||||
ConductarrPreference.updateFixtures(previousFixtures)
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test fun removedAnimeRootReturnsHomeWithoutDiscardingPushedPagesOrAvatarFocus() = runBlocking {
|
||||
val title = Destination.Wolfskin("title/tv%3A123")
|
||||
val person = Destination.Wolfskin("person/456")
|
||||
val scenarios = listOf(emptyList<Destination.Wolfskin>() to false, emptyList<Destination.Wolfskin>() to true,
|
||||
listOf(title) to false, listOf(title, person) to false)
|
||||
for ((pushed, avatarFocused) in scenarios) FakeConductarr().use { fake ->
|
||||
ConductarrPreference.updateBaseUrl(fake.url)
|
||||
ConductarrSession(MemoryCacheStore()).use { conductarr ->
|
||||
val context = ApplicationProvider.getApplicationContext<Application>()
|
||||
val server = JellyfinServer(UUID.randomUUID(), "Jellyfin", fake.url, null)
|
||||
val user = JellyfinUser(id = UUID.randomUUID(), name = "Shared", serverId = server.id, accessToken = SHARED_TOKEN)
|
||||
val repository = mockk<ServerRepository>(relaxed = true)
|
||||
coEvery { repository.serverDao.getServer(server.id) } returns null
|
||||
var hasTitles = true
|
||||
val libraries = mockk<LibraryReadFacade>(relaxed = true)
|
||||
coEvery { libraries.readDestinations(user.id) } returns emptyMap()
|
||||
coEvery { libraries.hasAnimeTitles(user.id) } coAnswers { hasTitles }
|
||||
val navigation = NavigationManager()
|
||||
val session = WolfskinSession()
|
||||
val model = WolfskinShellViewModel(context, session, repository, navigation, mockk(), mockk(relaxed = true),
|
||||
libraries, WolfskinSeeAllLists(), conductarr, mockk(relaxed = true))
|
||||
try {
|
||||
model.load(server, user)
|
||||
requireNotNull(model.onForeground(server, user)).join()
|
||||
val shell = requireNotNull(session.state)
|
||||
assertTrue(shell.person.hasAnime)
|
||||
shell.key(ShellKey.Ok)
|
||||
shell.focusNav(shell.tabs.indexOf("anime"))
|
||||
shell.key(ShellKey.Ok)
|
||||
assertEquals(listOf(Destination.Wolfskin("anime")), navigation.backStack.toList())
|
||||
for (page in pushed) {
|
||||
navigation.navigateTo(page)
|
||||
shell.enterDestination(page.route)
|
||||
}
|
||||
if (avatarFocused) shell.focusNav(shell.tabs.size)
|
||||
|
||||
hasTitles = false
|
||||
requireNotNull(model.onForeground(server, user)).join()
|
||||
assertFalse("The foreground read removed the last accessible Anime title", "anime" in shell.tabs)
|
||||
assertEquals("Home must replace the missing root, not the current pushed page",
|
||||
listOf(Destination.Home()) + pushed, navigation.backStack.toList())
|
||||
assertEquals(pushed.lastOrNull()?.route ?: "home", shell.route)
|
||||
assertEquals("The selected root must be Home even below a pushed page", 0, shell.currentTab)
|
||||
if (avatarFocused) assertEquals(shell.tabs.size, shell.navIndex)
|
||||
|
||||
shell.key(ShellKey.Back)
|
||||
if (pushed.isEmpty()) {
|
||||
assertTrue("Back from the replacement Home must enter its nav ladder", shell.navFocused)
|
||||
assertEquals("home", shell.route)
|
||||
assertEquals(listOf(Destination.Home()), navigation.backStack.toList())
|
||||
if (!avatarFocused) assertEquals(0, shell.navIndex)
|
||||
} else {
|
||||
assertEquals("Back pops only the current page and retains the replacement Home root",
|
||||
listOf(Destination.Home()) + pushed.dropLast(1), navigation.backStack.toList())
|
||||
}
|
||||
} finally { model.viewModelScope.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +181,14 @@ class WolfskinShellViewModel @Inject constructor(
|
|||
|
||||
private fun handleAction(action: ShellAction, server: JellyfinServer, user: JellyfinUser) {
|
||||
when (action) {
|
||||
is ShellAction.Destination -> if (action.route == "settings") navigation.navigateTo(Destination.Wolfskin("settings")) else navigation.replace(if (action.route == "home") Destination.Home() else Destination.Wolfskin(action.route))
|
||||
is ShellAction.Destination -> {
|
||||
val target = if (action.route == "home") Destination.Home() else Destination.Wolfskin(action.route)
|
||||
when {
|
||||
action.route == "settings" -> navigation.navigateTo(target)
|
||||
action.preservePushed && navigation.backStack.size > 1 -> navigation.backStack[0] = target
|
||||
else -> navigation.replace(target)
|
||||
}
|
||||
}
|
||||
ShellAction.Pop -> navigation.goBack()
|
||||
is ShellAction.HandOff -> navigation.navigateTo(handOffDestination(action.group))
|
||||
is ShellAction.Person -> selectPerson(action.person, server, user)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue