Fix Home navigation ownership and merge watch lanes
This commit is contained in:
parent
c2f7ccc438
commit
12c201dc55
41 changed files with 671 additions and 307 deletions
|
|
@ -110,12 +110,12 @@ class HomeCaptureTest {
|
|||
@Test fun st019_picksUnavailable() = capture("st-019", mode = "picks-unavailable", row = "trending")
|
||||
@Test fun st020_missingArt() = capture("st-020", person = "shared", mode = "missing", row = "lib-movies")
|
||||
@Test fun st096_widestTilePlate() = capture("st-096", person = "mindel", row = "new", column = 4)
|
||||
@Test fun st100_episodePlate() = capture("st-100", person = "mindel", row = "nu")
|
||||
@Test fun st100_episodePlate() = capture("st-100", person = "mindel", row = "cw")
|
||||
@Test fun st099_posterPlate() = capture("st-099", row = "lib-movies", column = 1)
|
||||
@Test fun st101_unratedEpisode() = capture("st-101", person = "mindel", row = "nu", column = 2)
|
||||
@Test fun st101_unratedEpisode() = capture("st-101", person = "mindel", row = "cw", column = 2)
|
||||
@Test fun st102_filmTile() = capture("st-102", person = "mindel", row = "new", column = 4)
|
||||
@Test fun st103_heroRatings() = capture("st-103")
|
||||
@Test fun spacingNextUpFirstFocused() = capture("spacing-next-up", row = "nu")
|
||||
@Test fun spacingNextUpFirstFocused() = capture("spacing-next-up", row = "cw")
|
||||
@Test fun focusHeroDetails() = capture("focus-hero-details", column = 1)
|
||||
@Test fun xHomeDiscoverStart() = capture("x-home-discover-start", row = "pk-m")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/** Bright artwork cannot reappear between a landscape title scrim and the card's solid state edge. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class HomeGradientTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test fun focusedAndUnfocusedTileAndWideScrimsReachTheBottomWithoutDarkeningTheUpperImage() {
|
||||
val bitmap = Bitmap.createBitmap(8, 8, Bitmap.Config.ARGB_8888).apply { eraseColor(android.graphics.Color.WHITE) }
|
||||
val art = mapOf("solid" to bitmap.asImageBitmap())
|
||||
val title = HomeTitle("episode", "Episode", "episode", backdrop = "solid", seriesTitle = "Series")
|
||||
var row by mutableStateOf(HomeRow("gradient", HomeRowKind.Shelf, cards = listOf(title), cardKind = HomeCardKind.Tile))
|
||||
var focused by mutableStateOf(false)
|
||||
val memory = HomeViewMemory()
|
||||
rule.setContent { WolfskinTheme { CompositionLocalProvider(LocalTitleBitmaps provides art) {
|
||||
Box(Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight).background(WolfskinTokens.Color.bg)) {
|
||||
HomeShelf(row, memory, focused, 0, emptyList(), {}, {})
|
||||
}
|
||||
} } }
|
||||
val directory = kotlin.io.path.createTempDirectory("wolfskin-gradient-").toFile()
|
||||
try {
|
||||
for (kind in listOf(HomeCardKind.Tile, HomeCardKind.Wide)) for (selected in listOf(false, true)) {
|
||||
rule.runOnIdle { row = row.copy(cardKind = kind); focused = selected }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val bounds = rule.onNodeWithTag("home-card:gradient:0").fetchSemanticsNode().boundsInRoot
|
||||
val file = File(directory, "$kind-$selected.png")
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val rendered = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
val scale = bounds.height / row.cardHeight.value
|
||||
val x = (bounds.right - WolfskinTokens.Home.cardPadding.value * scale * 2).toInt()
|
||||
val bottom = (bounds.bottom - WolfskinTokens.CardMetadata.bottom.value * scale - scale).toInt()
|
||||
val upper = (bounds.top + bounds.height * .4f).toInt()
|
||||
assertTrue("$kind/$selected image leaks below the scrim", android.graphics.Color.red(rendered.getPixel(x, bottom)) < 80)
|
||||
assertTrue("$kind/$selected scrim starts too high", android.graphics.Color.red(rendered.getPixel(x, upper)) > 245)
|
||||
} finally { rendered.recycle() }
|
||||
}
|
||||
} finally { directory.deleteRecursively() }
|
||||
}
|
||||
}
|
||||
|
|
@ -98,10 +98,10 @@ class HomeKeysTest {
|
|||
var action: HomeAction? = null
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) { action = it } } } }
|
||||
press("DOWN")
|
||||
rule.onNodeWithTag("home-card:nu:2").performMouseInput { enter(center) }
|
||||
rule.onNodeWithTag("home-card:cw:2").performMouseInput { enter(center) }
|
||||
rule.waitForIdle()
|
||||
assertEquals("nu", snapshot.rows[shell.view.row].id)
|
||||
assertEquals(2, shell.view.cards["nu"])
|
||||
assertEquals("cw", snapshot.rows[shell.view.row].id)
|
||||
assertEquals(2, shell.view.cards["cw"])
|
||||
assertNull(action)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import androidx.compose.ui.MotionDurationScale
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
|
||||
/** Directional navigation follows stable rows through page returns and asynchronous replacement. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class HomeNavigationTest {
|
||||
private val motionScale = object : MotionDurationScale { override val scaleFactor = 1f }
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>(effectContext = motionScale)
|
||||
private val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
private val cards = listOf(HomeTitle("episode", "Episode", "episode", seriesTitle = "Series", season = 1, episode = 2))
|
||||
private val rows = listOf(
|
||||
HomeRow("hero", HomeRowKind.Hero, cards = cards),
|
||||
HomeRow("cw", HomeRowKind.Shelf, "Continue watching", cards, HomeCardKind.Wide),
|
||||
HomeRow("nu", HomeRowKind.Shelf, "Next up", cards, HomeCardKind.Tile),
|
||||
HomeRow("new", HomeRowKind.Shelf, "New in your library", cards, HomeCardKind.Tile),
|
||||
HomeRow("pending", HomeRowKind.Shelf, skeleton = true),
|
||||
HomeRow("section", HomeRowKind.Section),
|
||||
) + (0..5).map { HomeRow("row-$it", HomeRowKind.Shelf, "Shelf $it", cards) }
|
||||
private var snapshot by mutableStateOf(HomeSnapshot(rows))
|
||||
private var title by mutableStateOf<TitleState?>(null)
|
||||
private var outgoing by mutableStateOf<TitleState?>(null)
|
||||
private var retainOutgoing = false
|
||||
|
||||
@Test fun downScrollAfterBackIsMonotonicAtSixteenMillisecondFrames() {
|
||||
retainOutgoing = true
|
||||
mount()
|
||||
press(Key.DirectionDown)
|
||||
press(Key.DirectionCenter)
|
||||
press(Key.Back)
|
||||
rule.runOnIdle { outgoing = null }
|
||||
rule.waitForIdle()
|
||||
rule.mainClock.autoAdvance = false
|
||||
rule.onRoot().performKeyInput { pressKey(Key.DirectionDown) }
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "home-scroll-frames").apply { mkdirs() }
|
||||
val series = mutableListOf<String>("milliseconds,index,offset")
|
||||
var previousIndex = shell.view.listState.firstVisibleItemIndex
|
||||
var previousOffset = shell.view.listState.firstVisibleItemScrollOffset
|
||||
val start = previousIndex to previousOffset
|
||||
repeat((WolfskinTokens.Motion.scroll + 64) / 16) { frame ->
|
||||
rule.mainClock.advanceTimeBy(16)
|
||||
rule.waitForIdle()
|
||||
val list = shell.view.listState
|
||||
val index = list.firstVisibleItemIndex
|
||||
val offset = list.firstVisibleItemScrollOffset
|
||||
assertTrue("Scroll must not snap backward at frame $frame", index > previousIndex || index == previousIndex && offset >= previousOffset)
|
||||
series += "${(frame + 1) * 16},$index,$offset"
|
||||
rule.onRoot().captureRoboImage(File(directory, "frame-${frame.toString().padStart(2, '0')}.png").absolutePath)
|
||||
previousIndex = index
|
||||
previousOffset = offset
|
||||
}
|
||||
File(directory, "position.csv").writeText(series.joinToString("\n"))
|
||||
assertNotEquals("Down must actually scroll after Back", start, previousIndex to previousOffset)
|
||||
assertEquals(2, shell.view.row)
|
||||
rule.mainClock.autoAdvance = true
|
||||
walkDown(start = 2)
|
||||
}
|
||||
|
||||
@Test fun outgoingEpisodeDuringBackCannotTakeOverTheHomeScroller() {
|
||||
retainOutgoing = true
|
||||
mount()
|
||||
press(Key.DirectionDown)
|
||||
press(Key.DirectionDown)
|
||||
press(Key.DirectionCenter)
|
||||
rule.onNodeWithTag("title-page").assertIsDisplayed()
|
||||
press(Key.Back)
|
||||
rule.runOnIdle { outgoing = null }
|
||||
rule.waitForIdle()
|
||||
assertEquals(2, shell.view.row)
|
||||
walkDown(start = 2)
|
||||
}
|
||||
|
||||
@Test fun downAdvancesEveryRowAndScrollsAfterEpisodeBack() {
|
||||
mount()
|
||||
walkDown()
|
||||
repeat(rows.count { it.focusable } - 1) { press(Key.DirectionUp) }
|
||||
press(Key.DirectionDown)
|
||||
val saved = shell.view
|
||||
press(Key.DirectionCenter)
|
||||
rule.onNodeWithTag("title-page").assertIsDisplayed()
|
||||
press(Key.Back)
|
||||
assertSame(saved, shell.view)
|
||||
assertEquals(1, shell.view.row)
|
||||
walkDown(start = 1)
|
||||
}
|
||||
|
||||
@Test fun refreshReorderInsertionRemovalAndSkeletonReadyKeepDownProgress() {
|
||||
mount()
|
||||
press(Key.DirectionDown)
|
||||
press(Key.DirectionDown)
|
||||
assertEquals("nu", snapshot.rows[shell.view.row].id)
|
||||
rule.runOnIdle { snapshot = snapshot.copy(rows = listOf(rows[0], rows[3], rows[1], rows[2]) + rows.drop(4)) }
|
||||
rule.waitForIdle()
|
||||
assertEquals("nu", snapshot.rows[shell.view.row].id)
|
||||
rule.runOnIdle { snapshot = snapshot.copy(rows = snapshot.rows.map { if (it.id == "pending") it.copy(cards = cards, skeleton = false) else it }) }
|
||||
press(Key.DirectionDown)
|
||||
assertEquals("pending", snapshot.rows[shell.view.row].id)
|
||||
rule.runOnIdle { snapshot = snapshot.copy(rows = listOf(HomeRow("insert", HomeRowKind.Shelf, cards = cards)) + snapshot.rows) }
|
||||
rule.waitForIdle()
|
||||
assertEquals("pending", snapshot.rows[shell.view.row].id)
|
||||
rule.runOnIdle { snapshot = snapshot.copy(rows = snapshot.rows.filter { it.id != "pending" }) }
|
||||
rule.waitForIdle()
|
||||
assertFalse(shell.navFocused)
|
||||
walkDown(start = shell.view.row)
|
||||
}
|
||||
|
||||
@Test fun loadingToReadyThenRefreshDoesNotTrapFocusOnPendingRows() {
|
||||
snapshot = HomeSnapshot(buildHomeRows(HomeLibrary(), emptyList(), loading = true))
|
||||
mount()
|
||||
assertTrue(shell.navFocused)
|
||||
rule.runOnIdle { snapshot = HomeSnapshot(rows) }
|
||||
rule.waitForIdle()
|
||||
assertFalse(shell.navFocused)
|
||||
walkDown()
|
||||
}
|
||||
|
||||
private fun mount() {
|
||||
rule.setContent { WolfskinTheme {
|
||||
val page = title
|
||||
if (page != null) ScreenEntry(shell, "title/episode", true) { TitleSurface(page, shell) }
|
||||
else ScreenEntry(shell, "home", true) { ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) { action ->
|
||||
if (action is HomeAction.Details) {
|
||||
shell.enterDestination("title/episode")
|
||||
title = TitleState(TitlePageData(TitleItem(action.title.id, action.title.title, "episode", held = true)), shell.person) { effect ->
|
||||
if (effect == TitleAction.Pop) {
|
||||
if (retainOutgoing) outgoing = title
|
||||
shell.enterDestination("home")
|
||||
title = null
|
||||
}
|
||||
}
|
||||
}
|
||||
} } }
|
||||
outgoing?.let { ScreenEntry(shell, "title/episode", false) { TitleSurface(it, shell) } }
|
||||
} }
|
||||
rule.waitForIdle()
|
||||
}
|
||||
|
||||
private fun walkDown(start: Int = 0) {
|
||||
val indices = snapshot.rows.indices.filter { it >= start && snapshot.rows[it].focusable }
|
||||
for (index in indices) {
|
||||
if (index != start) press(Key.DirectionDown)
|
||||
assertEquals("Down must advance to ${snapshot.rows[index].id}", index, shell.view.row)
|
||||
if (snapshot.rows[index].kind == HomeRowKind.Shelf) {
|
||||
val bounds = rule.onNodeWithTag("home-card:${snapshot.rows[index].id}:0").fetchSemanticsNode().boundsInRoot
|
||||
assertTrue("Focused card must clear navigation: $bounds", bounds.top >= WolfskinTokens.Layout.navBand.value * rule.density.density)
|
||||
assertTrue("Focused card must clear screen bottom: $bounds", bounds.bottom <= WolfskinTokens.Shell.stageHeight.value * rule.density.density)
|
||||
}
|
||||
}
|
||||
press(Key.DirectionDown)
|
||||
assertEquals(indices.last(), shell.view.row)
|
||||
}
|
||||
|
||||
private fun press(key: Key) {
|
||||
rule.onRoot().performKeyInput { pressKey(key) }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,26 @@ class HomeRowsTest {
|
|||
|
||||
@Test fun firstReadSkeletonHasNoFocusableContent() {
|
||||
val rows = buildHomeRows(HomeLibrary(), continuation, loading = true)
|
||||
assertEquals(listOf(HomeRowKind.Hero, HomeRowKind.Shelf, HomeRowKind.Shelf, HomeRowKind.Shelf, HomeRowKind.Shelf), rows.map { it.kind })
|
||||
assertTrue(rows.all { !it.focusable })
|
||||
}
|
||||
|
||||
@Test fun mergedWatchRowPrefersResumePerSeriesAndInterleavesByWatchRecency() {
|
||||
val inProgress = next.copy(id = "resume-a", seriesId = "a", resumeSeconds = 40.0, lastPlayedAt = 30)
|
||||
val olderResume = inProgress.copy(id = "older-a", lastPlayedAt = 10)
|
||||
val nextSameSeries = next.copy(id = "next-a", seriesId = "a", lastPlayedAt = 50)
|
||||
val nextOtherSeries = next.copy(id = "next-b", seriesId = "b", lastPlayedAt = 40)
|
||||
val film = resume.copy(id = "film", resumeSeconds = 20.0, lastPlayedAt = 20)
|
||||
val unknown = next.copy(id = "unknown", seriesId = "c")
|
||||
val library = HomeLibrary(listOf(inProgress, olderResume, film), listOf(nextSameSeries, nextOtherSeries, nextOtherSeries, unknown))
|
||||
assertEquals(listOf("next-b", "resume-a", "film", "unknown"), continueWatching(library).map { it.id })
|
||||
val rows = buildHomeRows(library, continuation)
|
||||
assertFalse(rows.any { it.id == "nu" })
|
||||
assertEquals(listOf("next-b", "film", "unknown"), rows.single { it.id == "cw" }.cards.map { it.id })
|
||||
}
|
||||
|
||||
@Test fun oldCachedEpisodesWithoutSeriesIdsDeduplicateBySeriesName() {
|
||||
val checkpoint = next.copy(id = "resume", seriesTitle = "Same series", resumeSeconds = 10.0)
|
||||
val following = next.copy(id = "next", seriesTitle = "Same series")
|
||||
assertEquals(listOf(checkpoint), continueWatching(HomeLibrary(listOf(checkpoint), listOf(following))))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** A retained outgoing title cannot steal another screen's viewport or Android input on Back. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class ScreenEntryNavigationTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
private val credits = (0..35).map { TitleItem("credit-$it", "Credit $it", "movie") }
|
||||
private val grid = LibraryGridState("Library", credits.map { LibraryTitle(it.id, it.name, "movie") }).apply { toolbar = false }
|
||||
private val person = PersonState(TitlePerson("actor", "Actor"), credits, {})
|
||||
private val title = TitleState(TitlePageData(TitleItem("parent", "Parent", "movie"), cast = listOf(TitlePerson("actor", "Actor")), related = credits.take(10)), shell.person) {}
|
||||
private val fixture = discoverTestFixture()
|
||||
private val snapshot = fixture.snapshot("justin")
|
||||
private val discover = DiscoverState(shell, snapshot, RecordingDiscoverOperations(snapshot, "justin"), CoroutineScope(Dispatchers.Main.immediate), {}, {}, fixture::service)
|
||||
private var childVisible by mutableStateOf(false)
|
||||
private var outgoingVisible by mutableStateOf(false)
|
||||
private lateinit var child: TitleState
|
||||
|
||||
@Test fun libraryBackRestoresCardAndDownReachesFinalGridRow() = verify("movies")
|
||||
@Test fun discoverBackRestoresShelfAndDownReachesFinalShelf() = verify("discover")
|
||||
@Test fun personBackRestoresCreditAndDownReachesFinalGridRow() = verify("person/actor")
|
||||
@Test fun titleBackRestoresRowAndDownReachesRelatedShelf() = verify("title/parent")
|
||||
|
||||
private fun verify(route: String) {
|
||||
shell.enterDestination(route)
|
||||
if (route == "discover") discover.focus(discover.firstShelf, 0)
|
||||
child = TitleState(TitlePageData(TitleItem("child", "Child episode", "episode", held = true)), shell.person) { action ->
|
||||
if (action == TitleAction.Pop) {
|
||||
shell.enterDestination(route)
|
||||
childVisible = false
|
||||
outgoingVisible = true
|
||||
}
|
||||
}
|
||||
rule.setContent { WolfskinTheme {
|
||||
if (!childVisible) ScreenEntry(shell, route, true) { host(route) }
|
||||
if (childVisible || outgoingVisible) ScreenEntry(shell, "title/child", childVisible) { TitleSurface(child, shell) }
|
||||
} }
|
||||
rule.waitForIdle()
|
||||
val view = shell.view
|
||||
val before = position(route)
|
||||
rule.runOnIdle { shell.enterDestination("title/child"); childVisible = true }
|
||||
press(Key.Back)
|
||||
rule.runOnIdle { outgoingVisible = false }
|
||||
rule.waitForIdle()
|
||||
assertSame(view, shell.view)
|
||||
assertEquals(before, position(route))
|
||||
repeat(maxOf(credits.size, discover.rowCount)) {
|
||||
val previous = position(route)
|
||||
press(Key.DirectionDown)
|
||||
assertTrue("$route Down cannot move backward: $previous -> ${position(route)}", position(route) >= previous)
|
||||
}
|
||||
val expected = when {
|
||||
route == "movies" -> (grid.items.lastIndex / grid.columns) * grid.columns
|
||||
route == "discover" -> discover.rowCount - 1
|
||||
route.startsWith("person/") -> (credits.lastIndex / WolfskinTokens.Behaviour.gridColumns.toInt()) * WolfskinTokens.Behaviour.gridColumns.toInt()
|
||||
else -> title.rows.lastIndex
|
||||
}
|
||||
assertEquals("$route must reach its last row after Back", expected, position(route))
|
||||
assertTrue("$route must scroll its last row into composition", view.listState.firstVisibleItemIndex > 0)
|
||||
}
|
||||
|
||||
@Composable private fun host(route: String) {
|
||||
when {
|
||||
route == "movies" -> ShellSurface(shell) { LibraryScreen(shell, grid, it) }
|
||||
route == "discover" -> ShellSurface(shell) { DiscoverScreen(discover, it) }
|
||||
route.startsWith("person/") -> PersonSurface(person, shell)
|
||||
else -> TitleSurface(title, shell)
|
||||
}
|
||||
}
|
||||
|
||||
private fun position(route: String): Int = when {
|
||||
route == "movies" -> grid.index
|
||||
route == "discover" -> discover.focusedRow
|
||||
route.startsWith("person/") -> person.index
|
||||
else -> title.rows.indexOf(title.row)
|
||||
}
|
||||
|
||||
private fun press(key: Key) {
|
||||
rule.onRoot().performKeyInput { pressKey(key) }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue