Fix TV artwork, episode layout and focus visibility

This commit is contained in:
justinzeus 2026-11-06 20:25:50 +01:00
parent 34087d8eb5
commit 9d2471c5b1
28 changed files with 466 additions and 117 deletions

View file

@ -0,0 +1,95 @@
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.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
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
/** Row ends, mixed card widths and real title Back navigation keep the selected art inside the view. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class HomeViewportTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test fun rowEndsRowChangesAndTitleBackPreserveTheFocusedCardMargin() {
val cards = HomeFixture(rule.activity).snapshot("justin").rows.flatMap { it.cards }.distinctBy { it.id }.take(12)
val rows = HomeCardKind.entries.flatMap { kind ->
val slots = when (kind) { HomeCardKind.Poster -> 6; HomeCardKind.Tile -> 4; HomeCardKind.Wide -> 3 }
listOf(slots, cards.size).map { count -> HomeRow("${kind.name}-$count", HomeRowKind.Shelf,
"${kind.name} shelf", cards.take(count), kind) }
}
val snapshot = HomeSnapshot(rows)
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false }
var title by mutableStateOf<TitleState?>(null)
rule.setContent { WolfskinTheme {
val page = title
if (page != null) TitleSurface(page, shell)
else ShellSurface(shell) { modifier -> HomeScreen(shell, snapshot, modifier) { action ->
if (action is HomeAction.Details) {
shell.enterDestination("title/viewport")
title = TitleState(TitlePageData(TitleItem(action.title.id, action.title.title, "movie", held = true)), shell.person) { effect ->
if (effect == TitleAction.Pop) { shell.enterDestination("home"); title = null }
}
}
} }
} }
for ((index, row) in rows.withIndex()) {
if (index > 0) press(Key.DirectionDown)
assertVisible(shell, rows)
repeat(row.count) { press(Key.DirectionLeft) }
assertVisible(shell, rows)
repeat(row.count - 1) { press(Key.DirectionRight) }
assertVisible(shell, rows)
capture("viewport-${row.id}")
press(Key.DirectionCenter)
rule.onNodeWithTag("title-page").assertIsDisplayed()
press(Key.Back)
assertVisible(shell, rows)
}
repeat(rows.lastIndex) { press(Key.DirectionUp); assertVisible(shell, rows) }
}
private fun press(key: Key) {
rule.onRoot().performKeyInput { pressKey(key) }
rule.mainClock.advanceTimeBy(1000)
rule.waitForIdle()
}
private fun assertVisible(shell: ShellState, rows: List<HomeRow>) {
rule.mainClock.advanceTimeBy(1000)
rule.waitForIdle()
val row = rows[shell.view.row]
val column = shell.view.cards[row.id] ?: 0
val bounds = rule.onNodeWithTag("home-card:${row.id}:$column").fetchSemanticsNode().boundsInRoot
val density = rule.density.density
val margin = WolfskinTokens.Card.posterGap.value * density
val stage = WolfskinTokens.Shell.stageWidth.value * density
assertTrue("Focused card is clipped: ${row.id}:$column $bounds", bounds.width >= row.cardWidth.value * density)
assertTrue("Focused card needs side margins: ${row.id}:$column $bounds", bounds.left >= margin && bounds.right <= stage - margin)
assertTrue("Focused row must clear the nav: ${row.id}:$column $bounds", bounds.top >= WolfskinTokens.Layout.navBand.value * density)
assertTrue("Focused card must clear the bottom: ${row.id}:$column $bounds", bounds.bottom <= WolfskinTokens.Shell.stageHeight.value * density - margin)
}
private fun capture(name: String) {
val directory = File(System.getProperty("wolfskin.captureDirectory"), "home").apply { mkdirs() }
rule.onRoot().captureRoboImage(File(directory, "$name.png").absolutePath)
}
}