Preserve episode height and fix title overlay and poster collisions
All checks were successful
review/code-claude Blockers fixed on a159c835, delta reviewed
review/design-claude Blockers fixed on a159c835, delta reviewed
All checks were successful
review/code-claude Blockers fixed on a159c835, delta reviewed
review/design-claude Blockers fixed on a159c835, delta reviewed
This commit is contained in:
parent
6d7513bd0e
commit
a159c835c5
14 changed files with 118 additions and 26 deletions
|
|
@ -223,4 +223,6 @@ class TitleCaptureTest {
|
|||
@Test fun round2MussoliniResume() = capture(121, "episode-episode-1-2025-s1e1", "shared", transform = { it.copy(episodes = it.episodes.distinctBy { episode -> episode.id }) })
|
||||
@Test fun round2Episodes() = capture(122, "episode-episode-1-2025-s1e1", "shared", transform = { it.copy(episodes = it.episodes.distinctBy { episode -> episode.id }) }) { state, _ -> state.focus("episodes", 0) }
|
||||
@Test fun round2NotHeldSeries() = capture(123, "90 Day Fiancé: Before the 90 Days")
|
||||
@Test fun round2HeldPosterFallback() = capture(124, "The Girl Who Leapt Through Time", transform = { it.copy(item = it.item.copy(backdrop = null, isPosterBackdrop = true, overview = "Short overview.")) })
|
||||
@Test fun round2NotHeldShortOverview() = capture(125, "The Amazing Spider-Man", transform = { it.copy(item = it.item.copy(backdrop = null, isPosterBackdrop = true, overview = "Short overview.")) })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,11 +88,12 @@ class TitleRound2Test {
|
|||
val actions = rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot
|
||||
val cast = rule.onNodeWithTag("title-cast").fetchSemanticsNode().boundsInRoot
|
||||
val gap = cast.top - actions.bottom
|
||||
assertTrue("Cast must follow the action caption and normal section gap: $gap", gap in 40f..110f)
|
||||
assertTrue("Cast follows the actions without the old fixed header reservation", gap > 0 && cast.top < WolfskinTokens.Layout.row_dhead.value * 2)
|
||||
rule.runOnIdle { state.update(state.data.copy(requestedStage = "Searching", requestActor = "Justin")) }
|
||||
val requestedActions = rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot
|
||||
val requestedCast = rule.onNodeWithTag("title-cast").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals("An actual readiness line belongs in the stack, not a fixed empty header", gap, requestedCast.top - requestedActions.bottom, 1f)
|
||||
assertTrue(requestedActions.bottom < requestedCast.top)
|
||||
assertTrue("A readiness line cannot move Cast backwards or recreate the old reservation", requestedCast.top >= cast.top && requestedCast.top < WolfskinTokens.Layout.row_dhead.value * 2)
|
||||
}
|
||||
|
||||
@Test fun mussoliniResumeHasOnlyFactsRatingsAndNoRedundantSeasonsAction() {
|
||||
|
|
@ -126,7 +127,8 @@ class TitleRound2Test {
|
|||
rule.runOnIdle { state.update(state.data.copy(episodesStatus = TitleReadStatus.Ready)) }
|
||||
val ready = rule.onNodeWithTag("title-episode-slot-0").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(skeleton, ready)
|
||||
assertEquals(16f / 9f, ready.width / ready.height, 0.01f)
|
||||
assertEquals(448f, ready.width, 0.1f)
|
||||
assertEquals("Wider cards retain the approved taller episode art", 280f, ready.height, 0.1f)
|
||||
rule.runOnIdle { state.focus("episodes", 0) }
|
||||
val episode = state.data.episodes.first()
|
||||
rule.onNodeWithText(episode.episodeLabel + " · " + episode.runtime).assertIsDisplayed()
|
||||
|
|
@ -143,6 +145,81 @@ class TitleRound2Test {
|
|||
rule.onNodeWithText("Container", useUnmergedTree = true).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun shortOverviewMoviesKeepCastBelowThePosterForHeldAndNotAddedTitles() {
|
||||
render("The Amazing Spider-Man") { it.copy(item = it.item.copy(overview = "Short overview.", backdrop = null, isPosterBackdrop = true)) }
|
||||
for (held in listOf(false, true)) {
|
||||
rule.runOnIdle { state.update(state.data.copy(item = state.data.item.copy(held = held))) }
|
||||
val poster = rule.onNodeWithTag("title-poster-backdrop").fetchSemanticsNode().boundsInRoot
|
||||
val firstActor = rule.onNodeWithTag("title-cast-${state.data.cast.first().id}").fetchSemanticsNode().boundsInRoot
|
||||
assertTrue("Cast clears the poster on held=$held: $firstActor vs $poster", firstActor.top >= poster.bottom + 20f - 0.5f)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun revokingTheOnlyMenuActionClosesAnOtherwiseEmptyMenu() {
|
||||
render("The Amazing Spider-Man")
|
||||
menu()
|
||||
rule.runOnIdle { state.update(state.data.copy(requestsAllowed = false)) }
|
||||
assertNull(state.sheet)
|
||||
rule.mainClock.advanceTimeBy(300)
|
||||
rule.onNodeWithTag("title-menu", useUnmergedTree = true).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test fun mediaInfoKeepsTableTextSeparatedInsteadOfAccumulatingOverlap() {
|
||||
render()
|
||||
menu()
|
||||
press(Key.DirectionCenter)
|
||||
rule.onNodeWithText("Container", useUnmergedTree = true).assertIsDisplayed()
|
||||
rule.onNodeWithText("Dutch", useUnmergedTree = true).assertIsDisplayed()
|
||||
val text = rule.onAllNodes(SemanticsMatcher.keyIsDefined(androidx.compose.ui.semantics.SemanticsActions.GetTextLayoutResult) and hasAnyAncestor(hasTestTag("title-sheet")), useUnmergedTree = true)
|
||||
.fetchSemanticsNodes().map { it.boundsInRoot }.filter { it.width > 0 && it.height > 0 }
|
||||
text.forEachIndexed { index, first -> text.drop(index + 1).forEach { second ->
|
||||
if (minOf(first.right, second.right) > maxOf(first.left, second.left)) {
|
||||
assertTrue("Media info text must never collide vertically: $first vs $second", minOf(first.bottom, second.bottom) <= maxOf(first.top, second.top))
|
||||
}
|
||||
} }
|
||||
}
|
||||
|
||||
@Test fun menuToMediaInfoNeverDoubleDimsOrPopsAndBackKeepsOneScrim() {
|
||||
render { it.copy(item = it.item.copy(backdrop = null, poster = null), cast = emptyList(), related = emptyList()) }
|
||||
menu()
|
||||
rule.mainClock.autoAdvance = false
|
||||
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")), "title-menu-info-motion").apply { mkdirs() }
|
||||
rule.runOnIdle { state.clickSheet(0) }
|
||||
var waited = 0
|
||||
while (rule.onAllNodesWithTag("title-sheet", useUnmergedTree = true).fetchSemanticsNodes().isEmpty() && waited <= WolfskinTokens.Motion.base + 100) {
|
||||
rule.mainClock.advanceTimeByFrame()
|
||||
rule.waitForIdle()
|
||||
waited += 16
|
||||
}
|
||||
val records = mutableListOf<String>()
|
||||
fun frame(phase: String, index: Int): Pair<Int, Int> {
|
||||
val path = File(directory, "$phase-${index.toString().padStart(2, '0')}.png")
|
||||
rule.onRoot().captureRoboImage(path.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(path.absolutePath)
|
||||
fun brightness(x: Int, y: Int): Int {
|
||||
val pixel = bitmap.getPixel(x, y)
|
||||
return android.graphics.Color.red(pixel) + android.graphics.Color.green(pixel) + android.graphics.Color.blue(pixel)
|
||||
}
|
||||
return try {
|
||||
(brightness(1900, 900) to brightness(100, 900)).also { records.add("$phase,${index * 16},${it.first},${it.second}") }
|
||||
} finally { bitmap.recycle() }
|
||||
}
|
||||
val info = List(14) { index ->
|
||||
rule.waitForIdle()
|
||||
frame("info", index).also { rule.mainClock.advanceTimeBy(16); rule.waitForIdle() }
|
||||
}
|
||||
info.forEach { assertEquals("No extra scrim or disappearing overlay may darken or pop over Media info", info.last(), it) }
|
||||
rule.runOnIdle { state.back(shell) }
|
||||
repeat(14) { index ->
|
||||
rule.mainClock.advanceTimeBy(16)
|
||||
rule.waitForIdle()
|
||||
assertEquals("Returning to Menu keeps exactly one scrim", info.last().second, frame("back", index).second)
|
||||
}
|
||||
rule.onNodeWithTag("title-menu", useUnmergedTree = true).assertExists()
|
||||
File(directory, "samples.csv").writeText("phase,timeMs,sheetBrightness,leftBrightness\n" + records.joinToString("\n"))
|
||||
rule.mainClock.autoAdvance = true
|
||||
}
|
||||
|
||||
@Test fun menuFadeFramesStayMonotonicAndNeverTranslate() {
|
||||
render { it.copy(item = it.item.copy(backdrop = null, poster = null), cast = emptyList(), related = emptyList()) }
|
||||
rule.mainClock.autoAdvance = false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue