Build the Wolfskin Home feed
This commit is contained in:
parent
9be55c570a
commit
0fd0ec035d
683 changed files with 2561 additions and 18 deletions
|
|
@ -0,0 +1,26 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** An unreadable local image exercises Coil's real error path without a network provider. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class HomeArtTest {
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
@Test fun imageFailureShowsTheTitleFallbackInsteadOfAnEmptyCard() {
|
||||
val title = HomeTitle("missing-image", "A missing film", "movie", poster = "file:///android_asset/wolfskin-home/not-present.jpg")
|
||||
rule.setContent { WolfskinTheme { HomeArt(title, WolfskinTokens.Card.posterW, WolfskinTokens.Card.posterH, Modifier.size(WolfskinTokens.Card.posterW, WolfskinTokens.Card.posterH)) } }
|
||||
rule.waitUntil(5000) { rule.onAllNodesWithText(title.title).fetchSemanticsNodes().isNotEmpty() }
|
||||
rule.onNodeWithText(title.title).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
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
|
||||
|
||||
/** Every frozen Home acceptance state renders the actual native Compose surface at density 2. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class HomeCaptureTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Before fun fullscreen() {
|
||||
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
|
||||
}
|
||||
|
||||
private fun capture(name: String, person: String = "justin", mode: String = "ready", row: String = "hero", column: Int = 0, nav: Int? = null) {
|
||||
val previous = Locale.getDefault()
|
||||
Locale.setDefault(Locale.US)
|
||||
try {
|
||||
val snapshot = HomeFixture(rule.activity).snapshot(person, mode)
|
||||
val shell = ShellState(fixturePeople(), person).apply {
|
||||
pickerVisible = false
|
||||
view.row = snapshot.rows.indexOfFirst { it.id == row }.coerceAtLeast(0)
|
||||
view.cards[row] = column
|
||||
if (nav != null) { navFocused = true; navIndex = nav }
|
||||
}
|
||||
preload(snapshot, shell.view.row)
|
||||
rule.setContent { WolfskinTheme {
|
||||
ShellSurface(shell, Modifier.requiredSize(WolfskinTokens.Shell.stageWidth, WolfskinTokens.Shell.stageHeight)) { modifier -> HomeScreen(shell, snapshot, modifier) {} }
|
||||
} }
|
||||
rule.mainClock.advanceTimeBy(1200)
|
||||
rule.waitForIdle()
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "home").apply { mkdirs() }
|
||||
rule.onRoot().captureRoboImage(File(directory, "$name.png").absolutePath)
|
||||
} finally { Locale.setDefault(previous) }
|
||||
}
|
||||
|
||||
private fun preload(snapshot: HomeSnapshot, focused: Int) = runBlocking {
|
||||
val rows = if (focused == 0) snapshot.rows.take(2) else snapshot.rows.drop(focused).take(3)
|
||||
for (row in rows) for (title in row.cards.take(8)) {
|
||||
val hero = row.kind == HomeRowKind.Hero
|
||||
val landscape = hero || row.cardKind != HomeCardKind.Poster
|
||||
val width = if (hero) 1280 else (row.cardWidth.value * 2).toInt()
|
||||
val height = if (hero) 720 else (row.cardHeight.value * 2).toInt()
|
||||
val source = if (landscape) title.backdrop ?: title.poster else title.poster
|
||||
if (source != null) HomeImages.loader(rule.activity).execute(HomeImages.request(rule.activity, source, width, height, !landscape))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun st005_loading() = capture("st-005", mode = "loading", nav = 0)
|
||||
@Test fun st006_justinNext() = capture("st-006")
|
||||
@Test fun st007_mindelResume() = capture("st-007", person = "mindel")
|
||||
@Test fun st008_sharedResume() = capture("st-008", person = "shared")
|
||||
@Test fun st009_deepTrending() = capture("st-009", row = "trending", column = 2)
|
||||
@Test fun st010_heroNav() = capture("st-010", nav = 0)
|
||||
@Test fun st011_deepBackNav() = capture("st-011", row = "lib-shows", column = 2, nav = 0)
|
||||
@Test fun st012_explore() = capture("st-012", row = "explore")
|
||||
@Test fun st013_unsigned() = capture("st-013", mode = "unsigned", row = "signin")
|
||||
@Test fun st014_discoverNav() = capture("st-014", nav = 1)
|
||||
@Test fun st015_offlineSaved() = capture("st-015", mode = "offline")
|
||||
@Test fun st016_conductarrDown() = capture("st-016", mode = "cond-down", row = "cond-down")
|
||||
@Test fun st017_conductarrSaved() = capture("st-017", mode = "cond-cached")
|
||||
@Test fun st018_jellyfinDown() = capture("st-018", mode = "jf-down")
|
||||
@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 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 st102_filmTile() = capture("st-102", person = "mindel", row = "new", column = 4)
|
||||
@Test fun st103_heroRatings() = capture("st-103")
|
||||
@Test fun xHomeDiscoverStart() = capture("x-home-discover-start", row = "pk-m")
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import org.json.JSONArray
|
||||
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
|
||||
|
||||
/** Approved replay data drives the same Android event handling used by the Home surface. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class HomeKeysTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test fun approvedHomePathsMatchFocusAndCounts() {
|
||||
val fixture = HomeFixture(rule.activity)
|
||||
var shell by mutableStateOf(ShellState(fixturePeople(), "justin"))
|
||||
var action: HomeAction? = null
|
||||
rule.setContent { WolfskinTheme {
|
||||
val snapshot = remember(shell, shell.person) { fixture.snapshot(shell.person.key) }
|
||||
ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) { action = it } }
|
||||
} }
|
||||
val paths = javaClass.getResourceAsStream("/wolfskin-home-presses.json")!!.bufferedReader().use { JSONArray(it.readText()) }
|
||||
for (index in 0 until paths.length()) {
|
||||
val path = paths.getJSONObject(index)
|
||||
val keys = path.getJSONArray("keys")
|
||||
rule.runOnIdle { shell = ShellState(fixturePeople(), "justin"); action = null }
|
||||
for (key in 0 until keys.length()) press(keys.getString(key))
|
||||
assertEquals(path.getString("goal"), path.getInt("presses"), keys.length())
|
||||
when (index) {
|
||||
0 -> { assertEquals("mindel", shell.person.key); assertTrue(action is HomeAction.Play); assertTrue((action as HomeAction.Play).resume) }
|
||||
1 -> { assertTrue(action is HomeAction.Play); assertFalse((action as HomeAction.Play).resume); assertEquals("Yu Yu Hakusho", (action as HomeAction.Play).title.seriesTitle) }
|
||||
2 -> assertEquals("trending", fixture.snapshot("justin").rows[shell.view.row].id)
|
||||
3 -> { val row = fixture.snapshot("justin").rows[shell.view.row]; assertEquals("trending", row.id); assertEquals(row.cards.size, shell.view.cards[row.id]); assertTrue(row.hasSeeAll) }
|
||||
4 -> { val row = fixture.snapshot("justin").rows[shell.view.row]; assertEquals("lib-shows", row.id); assertEquals(2, shell.view.cards[row.id]); assertFalse(shell.navFocused) }
|
||||
5 -> assertEquals("discover", shell.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun cardsActivateOnlyOnReleaseAndMovementCancelsArming() {
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
val snapshot = HomeFixture(rule.activity).snapshot("justin")
|
||||
var action: HomeAction? = null
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) { action = it } } } }
|
||||
press("DOWN")
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { keyDown(Key.DirectionCenter) }
|
||||
assertNull(action)
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { keyUp(Key.DirectionCenter) }
|
||||
assertTrue(action is HomeAction.Details)
|
||||
rule.runOnIdle { action = null }
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { keyDown(Key.DirectionCenter); pressKey(Key.DirectionRight); keyUp(Key.DirectionCenter) }
|
||||
assertNull(action)
|
||||
}
|
||||
|
||||
@Test fun loadingHasNavFocus() {
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
val snapshot = HomeFixture(rule.activity).snapshot("justin", "loading")
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) {} } } }
|
||||
rule.waitForIdle()
|
||||
assertTrue(shell.navFocused)
|
||||
assertEquals(0, shell.navIndex)
|
||||
}
|
||||
|
||||
@Test fun returningHomeRestoresTheExactViewportAndArrowReentersVisibleContent() {
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
val snapshot = HomeFixture(rule.activity).snapshot("justin")
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { modifier ->
|
||||
if (shell.route == "home") HomeScreen(shell, snapshot, modifier) {} else ShellPlaceholder(shell.route, modifier)
|
||||
} } }
|
||||
rule.onNodeWithTag("wolfskin-home").performScrollToIndex(3)
|
||||
val view = shell.view
|
||||
val savedIndex = view.listState.firstVisibleItemIndex
|
||||
val savedOffset = view.listState.firstVisibleItemScrollOffset
|
||||
assertEquals(3, savedIndex)
|
||||
rule.runOnIdle { shell.enterDestination("movies") }
|
||||
rule.runOnIdle { shell.enterDestination("home") }
|
||||
rule.waitForIdle()
|
||||
assertSame(view, shell.view)
|
||||
assertEquals(savedIndex, view.listState.firstVisibleItemIndex)
|
||||
assertEquals(savedOffset, view.listState.firstVisibleItemScrollOffset)
|
||||
press("RIGHT")
|
||||
assertEquals("lib-movies", snapshot.rows[view.row].id)
|
||||
assertEquals(0, view.cards["lib-movies"])
|
||||
}
|
||||
|
||||
@Test fun pointerHoverMovesFocusWithoutActivatingTheCard() {
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
val snapshot = HomeFixture(rule.activity).snapshot("justin")
|
||||
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.waitForIdle()
|
||||
assertEquals("nu", snapshot.rows[shell.view.row].id)
|
||||
assertEquals(2, shell.view.cards["nu"])
|
||||
assertNull(action)
|
||||
}
|
||||
|
||||
private fun press(name: String) {
|
||||
val key = when (name) {
|
||||
"UP" -> Key.DirectionUp; "DOWN" -> Key.DirectionDown
|
||||
"LEFT" -> Key.DirectionLeft; "RIGHT" -> Key.DirectionRight
|
||||
"BACK" -> Key.Back; else -> Key.DirectionCenter
|
||||
}
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(key) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import java.util.Locale
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/** Cache boundaries protect visible ratings during offline browsing and fast repeated focus. */
|
||||
class HomeRatingCacheTest {
|
||||
private val title = HomeTitle("jellyfin-1", "A film", "movie", tmdbId = 42)
|
||||
private val ratings = listOf(HomeRating("tmdb", 7.8))
|
||||
|
||||
@Test fun cacheIsSharedByCatalogueIdentityAndExpiresAtSixHours() {
|
||||
var now = 0L
|
||||
val cache = HomeRatingCache { now }
|
||||
cache.save(title, ratings)
|
||||
assertEquals(ratings, cache.read(title.copy(id = "another-server-copy")))
|
||||
now = 6L * 60 * 60 * 1000 - 1
|
||||
assertEquals(ratings, cache.read(title))
|
||||
now++
|
||||
assertNull(cache.read(title))
|
||||
}
|
||||
|
||||
@Test fun cacheKeepsObservedAbsenceAndEvictsOldestBeyondCapacity() {
|
||||
var now = 0L
|
||||
val cache = HomeRatingCache { now++ }
|
||||
cache.save(title, emptyList())
|
||||
assertEquals(emptyList<HomeRating>(), cache.read(title))
|
||||
repeat(300) { index -> cache.save(title.copy(id = "other-$index", tmdbId = index + 100), ratings) }
|
||||
assertNull(cache.read(title))
|
||||
assertEquals(ratings, cache.read(title.copy(id = "last", tmdbId = 399)))
|
||||
}
|
||||
|
||||
@Test fun decimalScoresFollowLocaleButPercentagesDoNot() {
|
||||
assertEquals("7,8", ratings.single().format(Locale.forLanguageTag("nl-NL")))
|
||||
assertEquals("7.8", ratings.single().format(Locale.US))
|
||||
assertEquals("87%", HomeRating("fresh", 87.9, percentage = true).format(Locale.forLanguageTag("nl-NL")))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import java.util.Locale
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** Real text layout proves locale formatting and the focus dwell instead of testing copied strings. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class HomeRatingsUiTest {
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
@Test fun dutchDecimalPlateStaysOnOneLine() {
|
||||
val previous = Locale.getDefault()
|
||||
Locale.setDefault(Locale.forLanguageTag("nl-NL"))
|
||||
try {
|
||||
rule.setContent { WolfskinTheme { HomeRatingPlate(listOf(HomeRating("tmdb", 7.8)), true, Modifier.width(WolfskinTokens.Card.posterW)) } }
|
||||
val layouts = mutableListOf<TextLayoutResult>()
|
||||
rule.onNodeWithText("7,8").performSemanticsAction(SemanticsActions.GetTextLayoutResult) { it(layouts) }
|
||||
assertEquals(1, layouts.single().lineCount)
|
||||
rule.onNodeWithText("7.8").assertDoesNotExist()
|
||||
} finally { Locale.setDefault(previous) }
|
||||
}
|
||||
|
||||
@Test fun focusDwellDefersUncachedPlateButCachedFocusReturnsImmediately() {
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
val title = HomeTitle("dwell", "A film", "movie", ratings = listOf(HomeRating("tmdb", 7.8)))
|
||||
val snapshot = HomeSnapshot(listOf(HomeRow("shelf", HomeRowKind.Shelf, "Shelf", listOf(title))), HomeAvailability.Ready)
|
||||
rule.mainClock.autoAdvance = false
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) {} } } }
|
||||
rule.mainClock.advanceTimeBy(100)
|
||||
rule.onNodeWithText("7.8").assertDoesNotExist()
|
||||
rule.mainClock.advanceTimeBy(450)
|
||||
rule.onNodeWithText("7.8").assertExists()
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.Back) }
|
||||
rule.runOnIdle { assertTrue(shell.navFocused) }
|
||||
rule.mainClock.advanceTimeBy(32)
|
||||
rule.onNodeWithText("7.8").assertDoesNotExist()
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionDown) }
|
||||
rule.runOnIdle { assertFalse(shell.navFocused) }
|
||||
rule.mainClock.advanceTimeBy(32)
|
||||
rule.onNodeWithText("7.8").assertExists()
|
||||
}
|
||||
|
||||
@Test fun signedOutHomeNeverDrawsHeroOrPlateScores() {
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
|
||||
val title = HomeTitle("signed-out", "A film", "movie", ratings = listOf(HomeRating("tmdb", 7.8)))
|
||||
val rows = buildHomeRows(HomeLibrary(nextUp = listOf(title)), listOf(homeDiscoverState(HomeAvailability.SignedOut)))
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { modifier -> HomeScreen(shell, HomeSnapshot(rows), modifier) {} } } }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.onNodeWithText("7.8").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/** Hero precedence and empty/error transitions preserve useful, focusable Home content. */
|
||||
class HomeRowsTest {
|
||||
private val resume = HomeTitle("resume", "Resume", "movie")
|
||||
private val next = HomeTitle("next", "Next", "episode")
|
||||
private val newest = HomeTitle("new", "New", "movie")
|
||||
private val pick = HomeTitle("pick", "Pick", "movie")
|
||||
private val continuation = listOf(HomeRow("pk-m", HomeRowKind.Shelf, cards = listOf(pick)))
|
||||
|
||||
@Test fun heroChainUsesResumeThenNextThenNewThenPickAndNeverRepeatsHeroOnShelves() {
|
||||
val cases = listOf(
|
||||
HomeLibrary(listOf(resume), listOf(next), listOf(newest)) to resume,
|
||||
HomeLibrary(nextUp = listOf(next), newest = listOf(newest)) to next,
|
||||
HomeLibrary(newest = listOf(newest)) to newest,
|
||||
HomeLibrary() to pick,
|
||||
)
|
||||
for ((library, expected) in cases) {
|
||||
val rows = buildHomeRows(library, continuation)
|
||||
assertEquals(expected.id, rows.first().cards.single().id)
|
||||
assertFalse(rows.filter { it.id in setOf("cw", "nu", "new") }.flatMap { it.cards }.any { it.id == expected.id })
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun heldShelvesAreUnwatchedFirstAndSeeAllUsesFullDestinationCount() {
|
||||
val watched = newest.copy(id = "watched", played = true, addedAt = "2026-11-05")
|
||||
val older = newest.copy(id = "older", addedAt = "2026-01-01")
|
||||
val recent = newest.copy(id = "recent", addedAt = "2026-10-01")
|
||||
val rows = buildHomeRows(HomeLibrary(destinations = mapOf("movies" to listOf(watched, older, recent)), destinationTotals = mapOf("movies" to 84)), continuation)
|
||||
val shelf = rows.single { it.id == "lib-movies" }
|
||||
assertEquals(listOf("recent", "older", "watched"), shelf.cards.map { it.id })
|
||||
assertEquals(84, shelf.total)
|
||||
assertTrue(shelf.hasSeeAll)
|
||||
}
|
||||
|
||||
@Test fun unavailableLibraryKeepsDiscoverAndFocusableRecoveryInsteadOfHeldShelves() {
|
||||
val rows = buildHomeRows(HomeLibrary(listOf(resume), listOf(next), listOf(newest), mapOf("movies" to listOf(newest))), continuation, libraryAvailable = false)
|
||||
assertEquals(listOf("hero", "jf-down", "disc-h", "pk-m"), rows.map { it.id })
|
||||
assertTrue(rows.single { it.id == "jf-down" }.focusable)
|
||||
}
|
||||
|
||||
@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 })
|
||||
}
|
||||
}
|
||||
126
wolfskin-design/src/test/resources/wolfskin-home-presses.json
Normal file
126
wolfskin-design/src/test/resources/wolfskin-home-presses.json
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
[
|
||||
{
|
||||
"goal": "Resume (Mindel, from launch)",
|
||||
"keys": [
|
||||
"RIGHT",
|
||||
"OK",
|
||||
"OK"
|
||||
],
|
||||
"presses": 3,
|
||||
"note": "",
|
||||
"start": "app launch: Who's watching with Justin focused (index.html:357)",
|
||||
"replayed": "PASS",
|
||||
"source": "index.html:2152-2174 PRESSES, replayed 2026-11-05"
|
||||
},
|
||||
{
|
||||
"goal": "Play next (Justin has no resume)",
|
||||
"keys": [
|
||||
"OK",
|
||||
"OK"
|
||||
],
|
||||
"presses": 2,
|
||||
"note": "",
|
||||
"start": "app launch: Who's watching with Justin focused (index.html:357)",
|
||||
"replayed": "PASS",
|
||||
"source": "index.html:2152-2174 PRESSES, replayed 2026-11-05"
|
||||
},
|
||||
{
|
||||
"goal": "Round 3 \u00b7 Home hero \u2192 Trending (D2 split)",
|
||||
"keys": [
|
||||
"OK",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN"
|
||||
],
|
||||
"presses": 13,
|
||||
"note": "12 Downs after the launch OK",
|
||||
"start": "app launch: Who's watching with Justin focused (index.html:357)",
|
||||
"replayed": "PASS",
|
||||
"source": "index.html:2152-2174 PRESSES, replayed 2026-11-05"
|
||||
},
|
||||
{
|
||||
"goal": "Round 3 \u00b7 Trending \u2192 its See all card (D3: 20 cards)",
|
||||
"keys": [
|
||||
"OK",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"RIGHT"
|
||||
],
|
||||
"presses": 33,
|
||||
"note": "20 Rights from Trending",
|
||||
"start": "app launch: Who's watching with Justin focused (index.html:357)",
|
||||
"replayed": "PASS",
|
||||
"source": "index.html:2152-2174 PRESSES, replayed 2026-11-05"
|
||||
},
|
||||
{
|
||||
"goal": "Round 3 \u00b7 deep Home row: Back to the nav, Down to the same card",
|
||||
"keys": [
|
||||
"OK",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"RIGHT",
|
||||
"RIGHT",
|
||||
"BACK",
|
||||
"DOWN"
|
||||
],
|
||||
"presses": 9,
|
||||
"note": "2: Back, Down",
|
||||
"start": "app launch: Who's watching with Justin focused (index.html:357)",
|
||||
"replayed": "PASS",
|
||||
"source": "index.html:2152-2174 PRESSES, replayed 2026-11-05"
|
||||
},
|
||||
{
|
||||
"goal": "Open Discover",
|
||||
"keys": [
|
||||
"OK",
|
||||
"UP",
|
||||
"RIGHT",
|
||||
"OK"
|
||||
],
|
||||
"presses": 4,
|
||||
"note": "",
|
||||
"start": "app launch: Who's watching with Justin focused (index.html:357)",
|
||||
"replayed": "PASS",
|
||||
"source": "index.html:2152-2174 PRESSES, replayed 2026-11-05"
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue