Remove the competing Discover title preview
This commit is contained in:
parent
5f5b5ffd54
commit
c80afb98a2
7 changed files with 151 additions and 54 deletions
|
|
@ -8,6 +8,8 @@ import android.view.WindowManager
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
|
|
@ -48,7 +50,12 @@ class DiscoverRefreshMotionTest {
|
|||
@Test fun loadedCatalogueRefreshKeepsReadyActionsOpaqueAndStationary() {
|
||||
FakeConductarr().use { fake ->
|
||||
fake.localArt = true
|
||||
ConductarrSession(MemoryCacheStore()).use { session ->
|
||||
val stored = MemoryCacheStore()
|
||||
val cache = object : org.justinvisser.wolfskin.conductarr.CacheStore by stored {
|
||||
@Volatile var lost = false
|
||||
override fun read(scope: String, key: String) = if (lost) null else stored.read(scope, key)
|
||||
}
|
||||
ConductarrSession(cache).use { session ->
|
||||
session.select("server:shared", SHARED_TOKEN, fake.url)
|
||||
runBlocking { session.authenticate() }
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
|
|
@ -71,6 +78,7 @@ class DiscoverRefreshMotionTest {
|
|||
val bounds = rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot
|
||||
val pixels = IntArray(bounds.width.toInt() * bounds.height.toInt())
|
||||
val baseline = brightness(File(directory, "loaded.png"), bounds, pixels)
|
||||
cache.lost = true
|
||||
coEvery { facade.findHeld(any(), any()) } coAnswers { refreshLookup.await() }
|
||||
rule.runOnIdle {
|
||||
model.load(route, person, preferences, fixtures = false, force = true)
|
||||
|
|
@ -83,6 +91,16 @@ class DiscoverRefreshMotionTest {
|
|||
assertEquals("Loaded actions cannot replay their rise", bounds, rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot)
|
||||
assertEquals("Loaded actions cannot replay their fade", baseline, brightness(File(directory, "refresh-${index.toString().padStart(2, '0')}.png"), bounds, pixels))
|
||||
}
|
||||
refreshLookup.complete(null)
|
||||
rule.waitUntil(30_000) {
|
||||
Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
|
||||
model.title?.data?.watchReady == true
|
||||
}
|
||||
repeat(12) {
|
||||
rule.mainClock.advanceTimeByFrame()
|
||||
assertEquals(TitleReadStatus.Ready, model.title!!.data.status)
|
||||
}
|
||||
assertSameFrame(File(directory, "loaded.png"), File(directory, "refreshed.png"))
|
||||
} finally {
|
||||
model.viewModelScope.cancel()
|
||||
refreshLookup.complete(null)
|
||||
|
|
@ -134,6 +152,9 @@ class DiscoverRefreshMotionTest {
|
|||
model.title?.data?.watchTarget?.watched == refreshedWatched
|
||||
}
|
||||
assertEquals(refreshedWatched, model.title!!.watched)
|
||||
rule.mainClock.advanceTimeBy(640)
|
||||
assertEquals(TitleReadStatus.Ready, model.title!!.data.status)
|
||||
assertSameFrame(File(directory, "loaded.png"), File(directory, "refreshed.png"))
|
||||
} finally { model.viewModelScope.cancel() }
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +203,8 @@ class DiscoverRefreshMotionTest {
|
|||
}
|
||||
assertEquals(TitleReadStatus.Ready, model.title!!.data.status)
|
||||
assertTrue(model.title!!.options.any { it.id == "play" })
|
||||
rule.mainClock.advanceTimeBy(640)
|
||||
assertSameFrame(File(directory, "loaded.png"), File(directory, "refreshed.png"))
|
||||
} finally { model.viewModelScope.cancel(); refresh.complete(held) }
|
||||
}
|
||||
}
|
||||
|
|
@ -194,6 +217,12 @@ class DiscoverRefreshMotionTest {
|
|||
val art = Bitmap.createBitmap(8, 8, Bitmap.Config.ARGB_8888).apply { eraseColor(android.graphics.Color.DKGRAY) }.asImageBitmap()
|
||||
rule.setContent { WolfskinTheme {
|
||||
model.title?.let { state ->
|
||||
val presented = remember(model) { booleanArrayOf(false) }
|
||||
val status = state.data.status
|
||||
SideEffect {
|
||||
if (presented[0]) assertEquals("Every rendered refresh frame stays Ready", TitleReadStatus.Ready, status)
|
||||
if (status == TitleReadStatus.Ready) presented[0] = true
|
||||
}
|
||||
val paths = listOfNotNull(state.data.item.poster, state.data.item.backdrop) +
|
||||
state.data.cast.mapNotNull { it.portrait } +
|
||||
(state.data.related + state.data.episodes).flatMap { listOfNotNull(it.poster, it.backdrop) }
|
||||
|
|
@ -204,6 +233,15 @@ class DiscoverRefreshMotionTest {
|
|||
} }
|
||||
}
|
||||
|
||||
private fun assertSameFrame(before: File, after: File) {
|
||||
rule.onRoot().captureRoboImage(after.absolutePath)
|
||||
val first = BitmapFactory.decodeFile(before.absolutePath)
|
||||
val second = BitmapFactory.decodeFile(after.absolutePath)
|
||||
try {
|
||||
assertTrue("Same-data refresh must return the identical rendered frame", first.sameAs(second))
|
||||
} finally { first.recycle(); second.recycle() }
|
||||
}
|
||||
|
||||
private fun brightness(file: File, bounds: Rect, pixels: IntArray): Long {
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,31 @@ import org.robolectric.annotation.GraphicsMode
|
|||
class DiscoverTitleTimingTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test fun detailsPushKeepsTheOutgoingDiscoverHeroUntilNavigationReplacesIt() {
|
||||
val shell = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply {
|
||||
pickerVisible = false; pickerLaunch = false; enterDestination("discover")
|
||||
}
|
||||
var pushed = ""
|
||||
lateinit var discover: DiscoverState
|
||||
rule.setContent {
|
||||
discover = rememberDiscoverState(shell, { pushed = it }, {}, fixtures = true)
|
||||
WolfskinTheme { ScreenEntry(shell, "discover", true) {
|
||||
ShellSurface(shell) { DiscoverScreen(discover, it) }
|
||||
} }
|
||||
}
|
||||
rule.waitUntil(30_000) { discover.snapshot.status == DiscoverStatus.Ready }
|
||||
rule.mainClock.advanceTimeBy(640)
|
||||
rule.onNodeWithText("Details").assertIsDisplayed()
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "discover-entry").apply { mkdirs() }
|
||||
rule.onRoot().captureRoboImage(File(directory, "hero-before-push.png").absolutePath)
|
||||
rule.runOnIdle { discover.openTitle(requireNotNull(discover.hero)) }
|
||||
rule.waitForIdle()
|
||||
assertEquals(titleRoute("movie:1930"), pushed)
|
||||
rule.onNodeWithText("Details").assertIsDisplayed()
|
||||
rule.onNodeWithText("For someone else").assertDoesNotExist()
|
||||
rule.onRoot().captureRoboImage(File(directory, "hero-after-push.png").absolutePath)
|
||||
}
|
||||
|
||||
@Test fun discoverDetailsFillCastAndRelatedAfterAsyncReplyAndBackRestoresDiscover() {
|
||||
FakeConductarr().use { fake ->
|
||||
fake.localArt = true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ import android.os.Looper
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
|
|
@ -45,9 +52,13 @@ class TitleEntryBackTest {
|
|||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private lateinit var viewModel: WolfskinTitleViewModel
|
||||
private lateinit var conductarr: ConductarrSession
|
||||
private var homeModel: WolfskinHomeViewModel? = null
|
||||
private var previousFixtures: Boolean? = null
|
||||
|
||||
@After fun dispose() {
|
||||
if (::viewModel.isInitialized) viewModel.viewModelScope.cancel()
|
||||
homeModel?.viewModelScope?.cancel()
|
||||
previousFixtures?.let(ConductarrPreference::updateFixtures)
|
||||
if (::conductarr.isInitialized) conductarr.close()
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +67,66 @@ class TitleEntryBackTest {
|
|||
@Test fun predictiveTitleBackPopsOnlyOneEntry() = verifyOutgoingBack(personPage = false, predictive = true)
|
||||
@Test fun predictivePersonBackPopsOnlyOneEntry() = verifyOutgoingBack(personPage = true, predictive = true)
|
||||
|
||||
@Test fun homeTitleBackKeepsTheExactPreOpenViewport() = verifyHomeViewport(reduced = false)
|
||||
@Test fun reducedMotionHomeTitleBackKeepsTheExactPreOpenViewport() = verifyHomeViewport(reduced = true)
|
||||
|
||||
private fun verifyHomeViewport(reduced: Boolean) {
|
||||
val navigation = NavigationManager()
|
||||
val viewer = ShellPerson("justin", "Justin", "J", true)
|
||||
val shell = ShellState(listOf(viewer), viewer.key).apply { pickerVisible = false; pickerLaunch = false }
|
||||
val snapshot = HomeFixture(rule.activity).snapshot(viewer.key)
|
||||
conductarr = ConductarrSession(MemoryCacheStore())
|
||||
previousFixtures = ConductarrPreference.fixtures
|
||||
ConductarrPreference.updateFixtures(true)
|
||||
val server = com.github.damontecres.wholphin.data.model.JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null)
|
||||
val user = com.github.damontecres.wholphin.data.model.JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null)
|
||||
val source = WolfskinHomeViewModel(rule.activity, mockk(relaxed = true), mockk(), navigation, WolfskinSeeAllLists(), conductarr)
|
||||
homeModel = source
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, mockk(), TitleFixtureProvider(rule.activity), navigation, conductarr,
|
||||
mockk(relaxed = true), mockk(relaxed = true))
|
||||
val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null)
|
||||
rule.setContent { CompositionLocalProvider(LocalReducedMotion provides reduced) { WolfskinTheme {
|
||||
val onBack = rememberWolfskinBackHandler(shell, navigation)
|
||||
NavDisplay(backStack = navigation.backStack, onBack = onBack,
|
||||
entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator()),
|
||||
entryProvider = { destination -> NavEntry(destination) {
|
||||
val active = navigation.backStack.lastOrNull() == destination
|
||||
val route = (destination as? Destination.Wolfskin)?.route ?: "home"
|
||||
LaunchedEffect(destination, active) { if (active) shell.enterDestination(route) }
|
||||
ScreenEntry(shell, route, active) {
|
||||
if (destination is Destination.Home) ShellSurface(shell) { modifier ->
|
||||
HomeDestination(shell, server, user, modifier, source)
|
||||
} else TitleDestination(route, shell, preferences, viewModel = viewModel, fixtures = true)
|
||||
}
|
||||
} })
|
||||
} } }
|
||||
rule.waitUntil(30_000) {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
source.snapshot.rows.any { it.id == "cw" } && !shell.navFocused
|
||||
}
|
||||
rule.onRoot().performKeyInput { pressKey(Key.DirectionDown) }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val home = shell.rememberedView("home")
|
||||
val tag = "home-card:${snapshot.rows[home.row].id}:0"
|
||||
val before = home.listState.let { it.firstVisibleItemIndex to it.firstVisibleItemScrollOffset }
|
||||
val bounds = rule.onNodeWithTag(tag).fetchSemanticsNode().boundsInRoot
|
||||
val captures = File(System.getProperty("wolfskin.captureDirectory"), "home-title-back-${if (reduced) "reduced" else "animated"}").apply { mkdirs() }
|
||||
rule.onRoot().captureRoboImage(File(captures, "sdk${Build.VERSION.SDK_INT}-before.png").absolutePath)
|
||||
rule.onRoot().performKeyInput { pressKey(Key.DirectionCenter) }
|
||||
rule.waitUntil(30_000) {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
viewModel.title?.data?.status == TitleReadStatus.Ready
|
||||
}
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
dispatchBack(predictive = false)
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
rule.onRoot().captureRoboImage(File(captures, "sdk${Build.VERSION.SDK_INT}-after.png").absolutePath)
|
||||
assertEquals(before, home.listState.let { it.firstVisibleItemIndex to it.firstVisibleItemScrollOffset })
|
||||
assertEquals(bounds, rule.onNodeWithTag(tag).fetchSemanticsNode().boundsInRoot)
|
||||
}
|
||||
|
||||
@Test fun nativeBackKeyWaitsForSystemCompletionBeforePoppingTheTitle() {
|
||||
val (navigation, shell) = mountTitle(personPage = false)
|
||||
val before = navigation.backStack.toList()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue