Integrate libraries and correct Home review blockers

This commit is contained in:
justinzeus 2026-11-05 19:03:33 +01:00
commit 14a74ac43b
642 changed files with 2377 additions and 169 deletions

View file

@ -0,0 +1,81 @@
package org.justinvisser.wolfskin.tv
import android.app.Application
import androidx.activity.ComponentActivity
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
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.mockk
import java.util.UUID
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.justinvisser.wolfskin.design.*
import org.justinvisser.wolfskin.tv.read.LibraryReadFacade
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/** Home opens the complete shared grid and Back restores its original continuation card. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
class HomeSeeAllNavigationTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test fun personalMovieContinuationReachesTheEighteenthTitleAndReturnsToHome() {
val snapshot = HomeFixture(rule.activity).snapshot("justin")
val row = snapshot.rows.first { it.id == "pk-m" }
val navigation = NavigationManager()
val lists = WolfskinSeeAllLists()
val shell = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin") {
if (it == ShellAction.Pop) navigation.goBack()
}.apply {
pickerVisible = false
view.row = snapshot.rows.indexOf(row)
view.home.focusedKey = row.id
view.cards[row.id] = row.cards.size
}
val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null)
val user = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null)
val model = WolfskinHomeViewModel(rule.activity, mockk(), mockk(), navigation, lists)
val facade = mockk<LibraryReadFacade>()
rule.setContent {
val destination = navigation.backStack.last()
val route = (destination as? Destination.Wolfskin)?.route ?: "home"
LaunchedEffect(destination) { shell.enterDestination(route) }
WolfskinTheme { ShellSurface(shell) { modifier ->
if (destination is Destination.Home) HomeScreen(shell, snapshot, modifier) { model.act(it, shell, server, user) }
else WolfskinLibraryContent(rule.activity, route, shell, user, "fixture", facade, navigation, lists, modifier)
} }
}
rule.onNodeWithTag("home-card:pk-m:5").assertIsDisplayed()
press(Key.DirectionCenter)
assertEquals("seeall/justin/pk-m", (navigation.backStack.last() as Destination.Wolfskin).route)
val grid = requireNotNull(lists.get("justin/pk-m"))
assertEquals(18, grid.items.size)
repeat(2) { press(Key.DirectionDown) }
repeat(5) { press(Key.DirectionRight) }
rule.onNodeWithText("Bleach the Movie: Fade to Black").assertIsDisplayed()
assertEquals("movie-bleach-the-movie-fade-to-black-2008", grid.focusedTitle?.id)
press(Key.Back)
rule.waitForIdle()
assertTrue(navigation.backStack.last() is Destination.Home)
assertEquals(row.id, snapshot.rows[shell.view.row].id)
assertEquals(5, shell.view.cards[row.id])
rule.onNodeWithTag("home-card:pk-m:5").assertIsDisplayed()
}
private fun press(key: Key) {
rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(key) }
}
}

View file

@ -0,0 +1,49 @@
package org.justinvisser.wolfskin.tv
import android.app.Application
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
import org.justinvisser.wolfskin.design.LibraryTitle
import org.justinvisser.wolfskin.tv.read.LibrarySnapshotStore
/** A stale library remains readable offline without crossing server/person boundaries. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class)
class LibrarySnapshotStoreTest {
@Test fun savedCopyArrivesBeforeFailedRefreshAndFreshCopySkipsNetwork() = runBlocking {
var now = 1000L
val store = LibrarySnapshotStore(MemoryCacheStore()) { now }
val items = listOf(LibraryTitle("a", "Film A", "movie", year = 2007, runtimeSeconds = 6000, played = true))
store.read("server:justin", "movies") { items }.toList()
val fresh = store.read("server:justin", "movies") { error("Fresh snapshot must not read") }.toList()
assertEquals(items, fresh.single().items)
now += 11 * 60_000
val offline = store.read("server:justin", "movies") { throw java.io.IOException("offline") }.toList()
assertEquals(2, offline.size)
assertFalse(offline.first().unreachable)
assertTrue(offline.last().unreachable)
assertEquals(1000L, offline.last().savedAtMillis)
assertEquals(items, offline.last().items)
for (scope in listOf("server:mindel", "other-server:justin")) {
val isolated = store.read(scope, "movies") { throw java.io.IOException("offline") }.toList().single()
assertNull(isolated.savedAtMillis)
assertTrue(isolated.items.isEmpty())
}
}
@Test fun explicitRecoveryRefreshesEvenFreshEmptyDestination() = runBlocking {
val store = LibrarySnapshotStore(MemoryCacheStore()) { 1000L }
store.read("server:justin", "movies") { emptyList() }.toList()
val current = listOf(LibraryTitle("b", "New Film", "movie"))
val reads = store.read("server:justin", "movies", force = true) { current }.toList()
assertTrue(reads.first().items.isEmpty())
assertEquals(current, reads.last().items)
assertFalse(reads.last().unreachable)
}
}