Refine late metadata fades and synchronize loading motion
All checks were successful
review/code-claude PASS on 16684454, delta fdff85c8 reviewed
review/design-claude PASS on 16684454, delta fdff85c8 reviewed
All checks were successful
review/code-claude PASS on 16684454, delta fdff85c8 reviewed
review/design-claude PASS on 16684454, delta fdff85c8 reviewed
This commit is contained in:
parent
1668445418
commit
fdff85c8a1
7 changed files with 149 additions and 15 deletions
|
|
@ -6,6 +6,7 @@ import android.os.StrictMode
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -19,6 +20,7 @@ import androidx.compose.ui.input.key.Key
|
|||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
|
|
@ -114,6 +116,91 @@ class MotionSurfaceTest {
|
|||
} finally { directory.deleteRecursively() }
|
||||
}
|
||||
|
||||
@Test fun shelvesJoiningAnActiveLoadShareTheSameBreathingPhase() {
|
||||
rule.mainClock.autoAdvance = false
|
||||
val second = mutableStateOf(false)
|
||||
rule.setContent { WolfskinTheme {
|
||||
Box(Modifier.requiredSize(960.dp, 540.dp).background(WolfskinTokens.Color.bg)) {
|
||||
Box(Modifier.size(200.dp, 140.dp).skeletonPulse().background(WolfskinTokens.Color.surface2))
|
||||
if (second.value) Box(Modifier.offset(y = 200.dp).size(200.dp, 140.dp).skeletonPulse().background(WolfskinTokens.Color.surface2))
|
||||
}
|
||||
} }
|
||||
rule.mainClock.advanceTimeBy(600)
|
||||
update { second.value = true }
|
||||
val directory = kotlin.io.path.createTempDirectory("wolfskin-shared-pulse-").toFile()
|
||||
try {
|
||||
var previous: Int? = null
|
||||
repeat(2) { sample ->
|
||||
val file = File(directory, "$sample.png")
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
val first = bitmap.getPixel(100, 100)
|
||||
assertEquals("Late shelves must join the existing phase", first, bitmap.getPixel(100, 500))
|
||||
if (previous != null) assertNotEquals("The shared clock must still breathe", previous, first)
|
||||
previous = first
|
||||
} finally { bitmap.recycle() }
|
||||
rule.mainClock.advanceTimeBy(450)
|
||||
}
|
||||
} finally { directory.deleteRecursively() }
|
||||
}
|
||||
|
||||
@Test fun captionExitKeepsTranslationFixedAtRestAndDuringInterruptedEntry() {
|
||||
rule.mainClock.autoAdvance = false
|
||||
val title = mutableStateOf("Settled caption")
|
||||
val visible = mutableStateOf(true)
|
||||
rule.setContent { WolfskinTheme { Caption(title.value, "Facts", visible.value, Modifier.offset(40.dp, 80.dp)) } }
|
||||
rule.mainClock.advanceTimeBy(400)
|
||||
fun top() = rule.onNodeWithText(title.value).fetchSemanticsNode().boundsInRoot.top
|
||||
val settled = top()
|
||||
update { visible.value = false }
|
||||
rule.mainClock.advanceTimeBy(48)
|
||||
assertEquals("Exit cannot sink a settled caption", settled, top(), 0.01f)
|
||||
update { title.value = "Interrupted caption"; visible.value = true }
|
||||
rule.mainClock.advanceTimeBy(112)
|
||||
val interrupted = top()
|
||||
assertTrue("Entry must still be partway through its rise", interrupted > settled)
|
||||
update { visible.value = false }
|
||||
val exitTop = top()
|
||||
assertTrue("Exit must retain the unfinished rise, not snap to its baseline", exitTop > settled)
|
||||
rule.mainClock.advanceTimeBy(48)
|
||||
assertEquals("An interrupted entry must fade exactly where it stopped", exitTop, top(), 0.01f)
|
||||
}
|
||||
|
||||
@Test fun lateRatingsGenresAndOverviewFadeInsideTheirExistingSlots() {
|
||||
rule.mainClock.autoAdvance = false
|
||||
val series = TitleItem("series", "A series", "tv", held = true)
|
||||
val state = TitleState(TitlePageData(series, connection = TitleConnection.SignedIn), viewer) {}
|
||||
val shell = shell()
|
||||
rule.setContent { WolfskinTheme { TitleSurface(state, shell, Modifier.requiredSize(960.dp, 540.dp)) } }
|
||||
rule.mainClock.advanceTimeBy(500)
|
||||
val actions = rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot
|
||||
update { state.update(state.data.copy(item = series.copy(ratings = listOf(LibraryRating("rt", 88.0)), genres = listOf("Drama"), overview = "Late overview"))) }
|
||||
rule.mainClock.advanceTimeBy(16)
|
||||
val regions = listOf("88%", "Drama", "Late overview").map { rule.onNodeWithText(it).fetchSemanticsNode().boundsInRoot }
|
||||
val directory = kotlin.io.path.createTempDirectory("wolfskin-late-fade-").toFile()
|
||||
fun brightness(name: String): List<Long> {
|
||||
val file = File(directory, name)
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
return try { regions.map { bounds ->
|
||||
var sum = 0L
|
||||
for (y in bounds.top.toInt() until bounds.bottom.toInt()) for (x in bounds.left.toInt() until bounds.right.toInt()) {
|
||||
val pixel = bitmap.getPixel(x, y)
|
||||
sum += android.graphics.Color.red(pixel) + android.graphics.Color.green(pixel) + android.graphics.Color.blue(pixel)
|
||||
}
|
||||
sum
|
||||
} } finally { bitmap.recycle() }
|
||||
}
|
||||
try {
|
||||
val entering = brightness("entering.png")
|
||||
rule.mainClock.advanceTimeBy(240)
|
||||
brightness("settled.png").zip(entering).forEach { (settled, early) -> assertTrue("Late text must fade rather than pop", settled > early) }
|
||||
assertEquals(regions, listOf("88%", "Drama", "Late overview").map { rule.onNodeWithText(it).fetchSemanticsNode().boundsInRoot })
|
||||
assertEquals(actions, rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot)
|
||||
} finally { directory.deleteRecursively() }
|
||||
}
|
||||
|
||||
@Test fun animatingSkeletonDoesNotPerformMainThreadDiskOrNetworkWork() {
|
||||
val loading = mutableStateOf(false)
|
||||
rule.setContent { WolfskinTheme { Box(Modifier.size(200.dp, 140.dp).skeletonPulse(loading.value).background(WolfskinTokens.Color.surface2)) } }
|
||||
|
|
@ -128,6 +215,11 @@ class MotionSurfaceTest {
|
|||
finally { rule.runOnIdle { StrictMode.setThreadPolicy(requireNotNull(previous)) } }
|
||||
}
|
||||
|
||||
private fun update(block: () -> Unit) {
|
||||
rule.runOnIdle { block(); androidx.compose.runtime.snapshots.Snapshot.sendApplyNotifications() }
|
||||
rule.mainClock.advanceTimeByFrame()
|
||||
}
|
||||
|
||||
private fun shell() = ShellState(listOf(viewer), viewer.key).apply { pickerVisible = false; pickerLaunch = false; enterDestination("title/series") }
|
||||
private fun bounds() = listOf("title-overview", "title-actions", "title-shelf:episodes").map { rule.onNodeWithTag(it).fetchSemanticsNode().boundsInRoot }
|
||||
private fun pixel(file: File): Int {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue