Preserve title data and focus across updates
This commit is contained in:
parent
0c94e8f910
commit
6d7df0fb19
31 changed files with 747 additions and 147 deletions
|
|
@ -54,4 +54,17 @@ class ShellRoutingTest {
|
|||
assertEquals(Destination.Search("name"), resolveUpstreamDestination(Destination.Search("name"), enabled, libraries))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun titleRolloutOwnsOnlyFilmSeriesEpisodeAndPerson() {
|
||||
val id = UUID.fromString("00000000-0000-0000-0000-000000000001")
|
||||
for (kind in listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE, BaseItemKind.PERSON)) {
|
||||
val destination = Destination.MediaItem(id, kind)
|
||||
assertNull(resolveUpstreamDestination(destination, true, libraries))
|
||||
assertEquals(destination, resolveUpstreamDestination(destination, false, libraries))
|
||||
}
|
||||
for (kind in listOf(BaseItemKind.COLLECTION_FOLDER, BaseItemKind.AUDIO, BaseItemKind.MUSIC_ALBUM, BaseItemKind.BOX_SET)) {
|
||||
val destination = Destination.MediaItem(id, kind)
|
||||
assertEquals(destination, resolveUpstreamDestination(destination, true, libraries))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.test.onRoot
|
|||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import org.json.JSONObject
|
||||
import org.junit.Before
|
||||
import org.junit.After
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -43,14 +44,17 @@ import java.util.Locale
|
|||
class TitleCaptureTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private lateinit var fixture: TitleFixture
|
||||
private var previousLocale = Locale.getDefault()
|
||||
|
||||
@Before fun fullscreen() {
|
||||
previousLocale = Locale.getDefault()
|
||||
Locale.setDefault(Locale.US)
|
||||
rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
@Suppress("DEPRECATION")
|
||||
rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
fixture = TitleFixture(rule.activity.assets.open("wolfskin-titles.json").bufferedReader().use { JSONObject(it.readText()) })
|
||||
}
|
||||
@After fun restoreLocale() { Locale.setDefault(previousLocale) }
|
||||
|
||||
private fun capture(number: Int, title: String = "Locke", person: String = "justin", transform: (TitlePageData) -> TitlePageData = { it }, configure: (TitleState, ShellState) -> Unit = { _, _ -> }) {
|
||||
val data = transform(fixture.title(title, person))
|
||||
|
|
@ -75,7 +79,9 @@ class TitleCaptureTest {
|
|||
if (data.item.backdrop == null || data.item.isPosterBackdrop) data.item.poster?.let { add(Triple(it, 420, 630)) }
|
||||
data.cast.forEach { person -> person.portrait?.let { add(Triple(it, 200, 200)) } }
|
||||
data.related.forEach { item -> item.poster?.let { add(Triple(it, 256, 384)) } }
|
||||
data.episodes.forEach { item -> (item.backdrop ?: item.poster)?.let { add(Triple(it, 416, 234)) } }
|
||||
data.episodes.forEach { item ->
|
||||
(if (item.isPosterBackdrop) item.poster else item.backdrop)?.let { add(Triple(it, 416, 234)) }
|
||||
}
|
||||
}
|
||||
return requests.distinctBy { it.first }.associate { (path, width, height) ->
|
||||
val bitmap = rule.activity.assets.open(path.removePrefix("file:///android_asset/")).use { BitmapFactory.decodeStream(it)!! }
|
||||
|
|
@ -89,7 +95,7 @@ class TitleCaptureTest {
|
|||
private fun save(number: Int) {
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory")).apply { mkdirs() }
|
||||
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory"))).apply { mkdirs() }
|
||||
rule.onRoot().captureRoboImage(File(directory, "st-${number.toString().padStart(3, '0')}.png").absolutePath)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performMouseInput
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.*
|
||||
|
|
@ -184,4 +187,68 @@ class TitleKeysTest {
|
|||
assertEquals(TitleSheet.Version, state.sheet)
|
||||
assertTrue(effects.none { it is TitleAction.Version })
|
||||
}
|
||||
|
||||
@Test fun episodeWatchedToggleKeepsItsIdentityAndDoesNotAskAboutTheSeries() {
|
||||
val episode = requireNotNull(fixture.title("Andor", "justin").playTarget)
|
||||
render(episode.id)
|
||||
rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "watched" }) }
|
||||
press(Key.DirectionCenter)
|
||||
assertNull(state.sheet)
|
||||
val change = effects.single() as TitleAction.Watched
|
||||
assertEquals(episode.id, change.item.id)
|
||||
assertEquals(!episode.watched, change.watched)
|
||||
}
|
||||
|
||||
@Test fun aPendingSeasonSkipsItsEpisodeSkeletons() {
|
||||
render("Andor")
|
||||
rule.runOnIdle { state.showSeasonLoading() }
|
||||
press(Key.DirectionDown); press(Key.DirectionDown)
|
||||
assertEquals("cast", state.row)
|
||||
rule.runOnIdle { state.restoreSeason() }
|
||||
press(Key.DirectionUp)
|
||||
assertEquals("episodes", state.row)
|
||||
}
|
||||
|
||||
@Test fun aFailedOlderVersionWriteCannotUndoTheNewerChoice() {
|
||||
render()
|
||||
press(Key.DirectionRight); press(Key.DirectionCenter); press(Key.DirectionDown); press(Key.DirectionCenter)
|
||||
val first = effects.last() as TitleAction.Version
|
||||
press(Key.DirectionCenter); press(Key.DirectionUp); press(Key.DirectionCenter)
|
||||
val second = effects.last() as TitleAction.Version
|
||||
rule.runOnIdle { state.rejectPlayback(first); state.confirmPlayback(second) }
|
||||
assertEquals("v1", state.versionId)
|
||||
press(Key.DirectionCenter); press(Key.DirectionDown); press(Key.DirectionCenter)
|
||||
val failed = effects.last() as TitleAction.Version
|
||||
rule.runOnIdle { state.rejectPlayback(failed) }
|
||||
assertEquals("v1", state.versionId)
|
||||
}
|
||||
|
||||
@Test fun aNewRowUsesTheNearestHorizontalCardBeforeItsOwnRememberedCard() {
|
||||
render()
|
||||
repeat(4) { press(Key.DirectionRight) }
|
||||
val x = rule.onNodeWithTag("title-option-trailer").fetchSemanticsNode().boundsInRoot.center.x
|
||||
val cast = state.data.cast.take(WolfskinTokens.Card.personSlots)
|
||||
val nearest = cast.indices.minBy { index ->
|
||||
kotlin.math.abs(rule.onNodeWithTag("title-cast-${cast[index].id}").fetchSemanticsNode().boundsInRoot.center.x - x)
|
||||
}
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(nearest, state.column)
|
||||
press(Key.DirectionRight)
|
||||
val remembered = state.column
|
||||
press(Key.DirectionUp); press(Key.DirectionDown)
|
||||
assertEquals(remembered, state.column)
|
||||
}
|
||||
|
||||
@Test fun wheelThenRemoteLandsOnTheFirstVisibleFocusableRow() {
|
||||
render()
|
||||
rule.onNodeWithTag("title-page").performMouseInput { scroll(800f) }
|
||||
val expected = rule.runOnIdle {
|
||||
shell.view.listState.layoutInfo.visibleItemsInfo.first {
|
||||
it.key in state.rows && it.offset + it.size > WolfskinTokens.Layout.pinnedLine.value * 2
|
||||
}.key as String
|
||||
}
|
||||
assertNotEquals("header", expected)
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(expected, state.row)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import org.jellyfin.sdk.model.api.MediaStream
|
|||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
|
@ -71,7 +70,7 @@ class TitleReadFacadeTest {
|
|||
every { repository.currentUser } returns null
|
||||
coEvery { playback.getSelectedTracks(any(), any(), any()) } returns null
|
||||
val context = mockk<Context> { every { filesDir } returns directory.root }
|
||||
facade = TitleReadFacade(api, mockk<ImageUrlService>(), playback, repository, mockk<FavoriteWatchManager>(), context)
|
||||
facade = TitleReadFacade(api, mockk<ImageUrlService>(relaxed = true), playback, repository, mockk<FavoriteWatchManager>(), context)
|
||||
}
|
||||
|
||||
private fun file(id: UUID, kind: BaseItemKind = BaseItemKind.MOVIE) = BaseItemDto(
|
||||
|
|
@ -89,7 +88,7 @@ class TitleReadFacadeTest {
|
|||
coEvery { library.getItem(any()) } throws java.io.IOException("offline")
|
||||
val saved = facade.readSaved(firstId, preferences)!!
|
||||
val screen = TitleState(saved, ShellPerson("justin", "Justin", "J", false)) {}
|
||||
assertEquals("mkv".uppercase(), saved.versions.single().container)
|
||||
assertEquals("MKV", saved.versions.single().container)
|
||||
assertEquals(3, screen.audio?.index)
|
||||
assertEquals(listOf(8), saved.versions.single().subtitles.map { it.index })
|
||||
assertEquals(TitleConnection.SignedOut, saved.connection)
|
||||
|
|
@ -109,6 +108,19 @@ class TitleReadFacadeTest {
|
|||
assertEquals("A real file", facade.readSaved(firstId, preferences)?.item?.name)
|
||||
}
|
||||
|
||||
@Test fun personBiographyAndContinuousCreditsRemainReadableFromDiskWithoutAService() = runTest {
|
||||
val actor = BaseItemDto(id = firstId, type = BaseItemKind.PERSON, name = "Actor", overview = "Actual biography")
|
||||
coEvery { library.getItem(firstId) } returns successResponse(actor)
|
||||
coEvery { items.getItems(any<GetItemsRequest>()) } returns successQueryResult(listOf(file(secondId)))
|
||||
facade.readPerson(firstId, 0)
|
||||
coEvery { library.getItem(any()) } throws java.io.IOException("offline")
|
||||
coEvery { items.getItems(any<GetItemsRequest>()) } throws java.io.IOException("offline")
|
||||
val saved = facade.readPersonSaved(firstId)!!
|
||||
assertEquals("Actual biography", saved.person.biography)
|
||||
assertEquals(secondId.toString(), saved.credits.single().id)
|
||||
assertEquals("A real file", saved.credits.single().name)
|
||||
}
|
||||
|
||||
@Test fun explicitEpisodeStaysThePlayAndWatchTargetAcrossDifferentSeasonSelection() = runTest {
|
||||
val episode = file(firstId, BaseItemKind.EPISODE).copy(seriesId = seriesId, parentIndexNumber = 4, indexNumber = 9)
|
||||
val other = file(secondId, BaseItemKind.EPISODE).copy(seriesId = seriesId, parentIndexNumber = 2, indexNumber = 1)
|
||||
|
|
@ -124,5 +136,6 @@ class TitleReadFacadeTest {
|
|||
assertEquals(firstId.toString(), data.watchTarget.id)
|
||||
assertEquals(listOf(secondId.toString()), data.episodes.map { it.id })
|
||||
assertEquals(listOf(2), data.seasonNumbers)
|
||||
assertEquals(2, TitleState(data, ShellPerson("justin", "Justin", "J", false)) {}.season)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue