Merge main for the updater UI review
This commit is contained in:
commit
9061384ad1
29 changed files with 751 additions and 233 deletions
|
|
@ -0,0 +1,135 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.BitmapFactory
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertEquals
|
||||
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
|
||||
|
||||
/** Real white, dark and colourful art cannot leak through the card's opaque bottom edge. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class CardBottomPixelTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test fun realPosterBottomRowsStayOpaqueAtRestAndFocused() {
|
||||
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
|
||||
val focused = mutableStateOf(-1)
|
||||
val showArt = mutableStateOf(true)
|
||||
val state = mutableStateOf("unwatched")
|
||||
val names = listOf("annie-hall-1977", "locke-2014", "apocalypse-now-1979", "avatar-aang-the-last-airbender-2026")
|
||||
rule.setContent { WolfskinTheme {
|
||||
Box(Modifier.requiredSize(960.dp, 540.dp).background(WolfskinTokens.Color.bg)) {
|
||||
names.forEachIndexed { index, name ->
|
||||
LibraryPoster(LibraryTitle(name, name, "movie", poster = if (showArt.value) "/library/assets/movie-$name-poster.jpg" else null,
|
||||
played = state.value == "watched", progress = if (state.value == "resume") .43f else 0f),
|
||||
focused.value == index, false, false, Modifier.offset((48 + 148 * index).dp, 130.dp), ::fixtureArt)
|
||||
}
|
||||
}
|
||||
} }
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "card-pixels").apply { mkdirs() }
|
||||
for (watchState in listOf("unwatched", "watched", "resume")) for (selection in -1..3) {
|
||||
rule.runOnIdle { state.value = watchState; focused.value = selection; showArt.value = false }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val referenceFile = File(directory, "reference.png")
|
||||
rule.onRoot().captureRoboImage(referenceFile.absolutePath)
|
||||
val reference = BitmapFactory.decodeFile(referenceFile.absolutePath)
|
||||
rule.runOnIdle { showArt.value = true }
|
||||
rule.waitForIdle()
|
||||
val file = File(directory, "$watchState-focus-$selection.png")
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
names.forEachIndexed { index, name ->
|
||||
val focus = selection == index
|
||||
val left = 96 + 296 * index
|
||||
val bottom = if (focus) 659 else 643
|
||||
val edgeLeft = if (focus) left - 10 else left
|
||||
val edgeRight = if (focus) left + 266 else left + 256
|
||||
// The reference supplies only rounded-corner/ring coverage, never poster colours.
|
||||
for (x in edgeLeft until edgeRight) assertEquals("$name $watchState focus=$focus full bottom row ($x,$bottom)",
|
||||
reference.getPixel(x, bottom), bitmap.getPixel(x, bottom))
|
||||
val expected = if (focus) WolfskinTokens.Color.accent.toArgb() else WolfskinTokens.Color.bg.toArgb()
|
||||
for (x in left + 32 until left + 224) assertEquals("$name $watchState focus=$focus straight edge ($x,$bottom)",
|
||||
expected, bitmap.getPixel(x, bottom))
|
||||
if (watchState != "unwatched") {
|
||||
val fillY = if (focus) 655 else 640
|
||||
assertEquals("$name $watchState fill", WolfskinTokens.Color.accent.toArgb(), bitmap.getPixel(left + 32, fillY))
|
||||
val right = if (watchState == "resume") androidx.compose.ui.graphics.Color(0xFF3D3D3F) else WolfskinTokens.Color.accent
|
||||
assertEquals("$name $watchState track", right.toArgb(), bitmap.getPixel(left + 224, fillY))
|
||||
}
|
||||
}
|
||||
reference.recycle()
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun completedHomeProgressUsesTheSameWatchedStripAsLibraryCards() {
|
||||
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
|
||||
val path = "/library/assets/movie-annie-hall-1977-poster.jpg"
|
||||
val fraction = mutableStateOf(.5f)
|
||||
val art = mapOf(path to fixtureArt(path)!!)
|
||||
val memory = HomeViewMemory()
|
||||
rule.setContent { WolfskinTheme {
|
||||
androidx.compose.runtime.CompositionLocalProvider(LocalTitleBitmaps provides art) {
|
||||
Box(Modifier.requiredSize(960.dp, 540.dp).background(WolfskinTokens.Color.bg)) {
|
||||
val title = HomeTitle("home", "Home film", "movie", runtimeSeconds = 100,
|
||||
poster = path, played = true, resumeSeconds = fraction.value * 100.0)
|
||||
HomeShelf(HomeRow("progress", HomeRowKind.Shelf, "Progress", listOf(title)),
|
||||
memory, false, 0, emptyList(), {}, {})
|
||||
LibraryPoster(LibraryTitle("reference", "Reference", "movie", poster = path,
|
||||
played = true, progress = fraction.value), false, false, false,
|
||||
Modifier.offset(240.dp, 42.dp), ::fixtureArt)
|
||||
}
|
||||
}
|
||||
} }
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "card-pixels").apply { mkdirs() }
|
||||
for (progress in listOf(.5f, 1f, 1.5f)) {
|
||||
rule.runOnIdle { fraction.value = progress }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val home = rule.onNodeWithTag("home-card:progress:0").fetchSemanticsNode().boundsInRoot
|
||||
val reference = rule.onNodeWithTag("poster-reference").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(reference.width, home.width, .1f)
|
||||
assertEquals(reference.bottom, home.bottom, .1f)
|
||||
val file = File(directory, "home-progress-${(progress * 100).toInt()}.png")
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
val bottom = home.bottom.toInt()
|
||||
val band = if (progress >= 1f) 7 else 10
|
||||
for (y in bottom - band until bottom) for (x in 0 until home.width.toInt()) {
|
||||
assertEquals("Home/library strip progress=$progress ($x,$y)",
|
||||
bitmap.getPixel(reference.left.toInt() + x, y), bitmap.getPixel(home.left.toInt() + x, y))
|
||||
}
|
||||
val separator = if (progress >= 1f) WolfskinTokens.Color.bg else WolfskinTokens.Color.accent
|
||||
assertEquals("Completion changes the resume strip to the 3px watched strip",
|
||||
separator.toArgb(), bitmap.getPixel(home.left.toInt() + 32, bottom - 7))
|
||||
assertEquals(WolfskinTokens.Color.accent.toArgb(), bitmap.getPixel(home.left.toInt() + 32, bottom - 4))
|
||||
} finally { bitmap.recycle() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.BitmapFactory
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
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
|
||||
|
||||
/** Changing a shelf's selected identity must restart its focus-only caption reveal. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class CardCaptionMotionTest {
|
||||
@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
|
||||
rule.mainClock.autoAdvance = false
|
||||
}
|
||||
|
||||
@Test fun changingTitleWhileStillFocusedRestoresOpaqueCaptionPixels() {
|
||||
val title = mutableStateOf("Initial caption")
|
||||
rule.setContent { WolfskinTheme {
|
||||
Box(Modifier.requiredSize(960.dp, 540.dp).background(WolfskinTokens.Color.bg)) {
|
||||
CardCaption(title.value, 2007, "2h 56m", true, true,
|
||||
Modifier.offset(40.dp, 80.dp).width(128.dp).testTag("animated"), focusOnly = true)
|
||||
CardCaption(title.value, 2007, "2h 56m", true, true,
|
||||
Modifier.offset(40.dp, 180.dp).width(128.dp).testTag("reference"))
|
||||
}
|
||||
} }
|
||||
for ((index, selected) in listOf("Initial caption", "Second caption", "Third caption").withIndex()) {
|
||||
update { title.value = selected }
|
||||
rule.mainClock.advanceTimeBy(400)
|
||||
assertCaptionPixels("component-$index",
|
||||
rule.onNodeWithTag("animated").fetchSemanticsNode().boundsInRoot,
|
||||
rule.onNodeWithTag("reference").fetchSemanticsNode().boundsInRoot)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun movingAcrossAFocusedHomeShelfKeepsEachSelectedCaptionVisible() {
|
||||
val path = "/library/assets/movie-annie-hall-1977-poster.jpg"
|
||||
val cards = listOf(HomeTitle("first", "First shelf caption", "movie", 2007, 10560, poster = path),
|
||||
HomeTitle("second", "Second shelf caption", "movie", 2008, 11520, poster = path))
|
||||
val row = HomeRow("captions", HomeRowKind.Shelf, "Shelf", cards)
|
||||
val column = mutableStateOf(0)
|
||||
val memory = HomeViewMemory()
|
||||
val art = mapOf(path to fixtureArt(path)!!)
|
||||
rule.setContent { WolfskinTheme {
|
||||
CompositionLocalProvider(LocalTitleBitmaps provides art) {
|
||||
Box(Modifier.requiredSize(960.dp, 540.dp).background(WolfskinTokens.Color.bg)) {
|
||||
Box(Modifier.testTag("home-shelf")) {
|
||||
HomeShelf(row, memory, true, column.value, emptyList(), {}, {})
|
||||
}
|
||||
val selected = cards[column.value]
|
||||
CardCaption(selected.title, selected.year, selected.runtime, false, true,
|
||||
Modifier.offset(48.dp, 400.dp).width(128.dp).testTag("reference"))
|
||||
}
|
||||
}
|
||||
} }
|
||||
for ((step, selected) in listOf(0, 1, 0).withIndex()) {
|
||||
update { column.value = selected }
|
||||
rule.mainClock.advanceTimeBy(400)
|
||||
val title = cards[selected].title
|
||||
val actual = rule.onNode(hasText(title) and hasAnyAncestor(hasTestTag("home-shelf")), useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
val reference = rule.onNode(hasText(title) and hasAnyAncestor(hasTestTag("reference")), useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
assertCaptionPixels("home-$step", actual, reference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(block: () -> Unit) {
|
||||
rule.runOnIdle { block(); Snapshot.sendApplyNotifications() }
|
||||
rule.mainClock.advanceTimeByFrame()
|
||||
}
|
||||
|
||||
private fun assertCaptionPixels(name: String, actual: Rect, reference: Rect) {
|
||||
assertEquals(reference.width, actual.width, .1f)
|
||||
assertEquals(reference.height, actual.height, .1f)
|
||||
rule.waitForIdle()
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "caption-followup").apply { mkdirs() }
|
||||
val file = File(directory, "$name.png")
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
val foreground = WolfskinTokens.Color.fg.toArgb()
|
||||
var compared = 0
|
||||
for (y in 0 until reference.height.toInt()) for (x in 0 until reference.width.toInt()) {
|
||||
if (bitmap.getPixel(reference.left.toInt() + x, reference.top.toInt() + y) == foreground) {
|
||||
assertEquals("$name opaque title pixel ($x,$y)", foreground,
|
||||
bitmap.getPixel(actual.left.toInt() + x, actual.top.toInt() + y))
|
||||
compared++
|
||||
}
|
||||
}
|
||||
assertTrue("The reference must contain opaque title glyphs", compared >= 24)
|
||||
} finally { bitmap.recycle() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import java.io.File
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
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 androidx.compose.ui.unit.dp
|
||||
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
|
||||
|
||||
/** Caption fitting and row pitches are measured from the same surfaces used by the TV. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class CardGeometryTest {
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
@Test fun narrowCaptionsEllipsiseOnlyTheTitleAndKeepRuntimeAndCheck() {
|
||||
val width = mutableStateOf(105.dp)
|
||||
rule.setContent { WolfskinTheme {
|
||||
CardCaption("Steins;Gate: The Movie - Load Region of Déjà Vu", 2007, "999h 59m", true, true,
|
||||
Modifier.width(width.value).testTag("caption"))
|
||||
} }
|
||||
for (pixels in listOf(210, 327)) {
|
||||
rule.runOnIdle { width.value = (pixels / 2f).dp }
|
||||
rule.waitForIdle()
|
||||
val layouts = rule.onAllNodes(SemanticsMatcher.keyIsDefined(SemanticsActions.GetTextLayoutResult), useUnmergedTree = true).fetchSemanticsNodes().map { node ->
|
||||
val results = mutableListOf<TextLayoutResult>()
|
||||
node.config[SemanticsActions.GetTextLayoutResult].action!!.invoke(results)
|
||||
results.single()
|
||||
}
|
||||
val title = layouts.single { it.layoutInput.text.text.startsWith("Steins;") }
|
||||
assertEquals(1, title.lineCount)
|
||||
assertTrue("Only the long title ellipsises at $pixels", title.isLineEllipsized(0))
|
||||
for (facts in layouts.filter { it !== title }) {
|
||||
assertFalse("Metadata must not overflow at $pixels", facts.hasVisualOverflow)
|
||||
assertFalse(facts.isLineEllipsized(0))
|
||||
}
|
||||
val caption = rule.onNodeWithTag("caption").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(58f, caption.height, .1f)
|
||||
assertEquals(pixels.toFloat(), caption.width, .1f)
|
||||
val check = rule.onNodeWithContentDescription("Watched").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(24f, check.width, .1f)
|
||||
assertEquals(24f, check.height, .1f)
|
||||
assertTrue(check.right <= caption.right)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun unfocusedLibraryTitlesKeepCardWidthAndGridPitch() {
|
||||
val titles = List(12) { LibraryTitle("$it", "A long title occupying only one card column ${it.toString().padStart(2, '0')}", "movie", 2007, 10560, poster = "/library/assets/movie-annie-hall-1977-poster.jpg") }
|
||||
val shell = ShellState(fixturePeople(), "shared").apply { pickerVisible = false; enterDestination("movies") }
|
||||
val grid = LibraryGridState("Movies", titles)
|
||||
rule.setContent { WolfskinTheme { ShellSurface(shell) { LibraryScreen(shell, grid, it, offlineArt = ::fixtureArt) } } }
|
||||
rule.mainClock.advanceTimeBy(1000)
|
||||
rule.waitForIdle()
|
||||
val first = rule.onNodeWithTag("poster-0").fetchSemanticsNode().boundsInRoot
|
||||
val second = rule.onNodeWithTag("poster-6").fetchSemanticsNode().boundsInRoot
|
||||
// Focus scales around the unchanged slot centre.
|
||||
assertEquals(372f, first.top + (first.height - 384f) / 2, .1f)
|
||||
assertEquals(524f, second.top - (first.top + (first.height - 384f) / 2), .1f)
|
||||
for (index in 0..5) {
|
||||
val caption = rule.onNodeWithText(titles[index].title, useUnmergedTree = true).fetchSemanticsNode()
|
||||
assertEquals(256f, caption.boundsInRoot.width, .1f)
|
||||
assertEquals(794f, caption.boundsInRoot.top, .1f)
|
||||
}
|
||||
val focusedCaption = rule.onNodeWithText(titles[0].title, useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(20.48f, focusedCaption.top - first.bottom - first.height * .04f - 2f * 1.08f, .1f)
|
||||
assertEquals(26.48f, second.top - first.height * .04f - 2f * 1.08f - (focusedCaption.top + 58f), .1f)
|
||||
rule.runOnIdle { kotlinx.coroutines.runBlocking { shell.view.listState.scrollToItem(0, 204) } }
|
||||
rule.waitForIdle()
|
||||
val pinnedFirst = rule.onNodeWithTag("poster-0").fetchSemanticsNode().boundsInRoot
|
||||
val pinnedSecond = rule.onNodeWithTag("poster-6").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(168f, pinnedFirst.top, .1f)
|
||||
assertEquals(692f, pinnedSecond.top, .1f)
|
||||
assertEquals(1076f, pinnedSecond.bottom, .1f)
|
||||
}
|
||||
|
||||
@Test fun homePosterWideAndTileShelvesReserveTheirCaptionBands() {
|
||||
val kind = mutableStateOf(HomeCardKind.Poster)
|
||||
val loading = mutableStateOf(false)
|
||||
val memory = HomeViewMemory()
|
||||
rule.setContent { WolfskinTheme {
|
||||
Column {
|
||||
Box(Modifier.testTag("shelf")) {
|
||||
HomeShelf(HomeRow("fixture", HomeRowKind.Shelf, "Fixture", listOf(HomeTitle("one", "One", "movie", 2007, 10560)),
|
||||
kind.value, skeleton = loading.value), memory, false, 0, emptyList(), {}, {})
|
||||
}
|
||||
Box(Modifier.size(1.dp).testTag("next"))
|
||||
}
|
||||
} }
|
||||
for ((cardKind, expected) in listOf(HomeCardKind.Poster to 632f, HomeCardKind.Wide to 522f, HomeCardKind.Tile to 441f)) {
|
||||
for (pending in listOf(false, true)) {
|
||||
rule.runOnIdle { kind.value = cardKind; loading.value = pending }
|
||||
rule.waitForIdle()
|
||||
assertEquals("$cardKind loading=$pending", expected, rule.onNodeWithTag("shelf").fetchSemanticsNode().boundsInRoot.height, .1f)
|
||||
assertEquals(expected, rule.onNodeWithTag("next").fetchSemanticsNode().boundsInRoot.top, .1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun filmographyKeepsUnfocusedMetadataAndDistinguishesSeriesFromFilmRuntime() {
|
||||
val path = "/library/assets/movie-annie-hall-1977-poster.jpg"
|
||||
val item = mutableStateOf(TitleItem("credit", "A filmography credit", "movie", 2007, 10560,
|
||||
poster = path, held = true, watched = true))
|
||||
val art = mapOf(path to fixtureArt(path)!!)
|
||||
rule.setContent { WolfskinTheme {
|
||||
androidx.compose.runtime.CompositionLocalProvider(LocalTitleBitmaps provides art) {
|
||||
TitlePoster(item.value, false, alwaysShowCaption = true) {}
|
||||
}
|
||||
} }
|
||||
rule.onNodeWithText("A filmography credit").assertIsDisplayed()
|
||||
rule.onNodeWithText("2007").assertIsDisplayed()
|
||||
rule.onNodeWithText("2h 56m").assertIsDisplayed()
|
||||
rule.onNodeWithContentDescription("Watched").assertIsDisplayed()
|
||||
rule.runOnIdle { item.value = item.value.copy(kind = "tv", year = 2016, runtimeSeconds = 123456) }
|
||||
rule.onNodeWithText("2016").assertIsDisplayed()
|
||||
rule.onNodeWithText("2h 56m").assertDoesNotExist()
|
||||
rule.onNodeWithContentDescription("Watched").assertIsDisplayed()
|
||||
rule.runOnIdle { item.value = item.value.copy(resumeSeconds = 10) }
|
||||
rule.onNodeWithContentDescription("Watched").assertDoesNotExist()
|
||||
rule.onNodeWithText("A filmography credit").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun loadingCaptionBandsReserveSpaceWithoutPaintingBars() {
|
||||
val slots = listOf("poster" to Triple(128.dp, 192.dp, 48.dp),
|
||||
"wide" to Triple(272.dp, 153.dp, 32.dp), "tile" to Triple(200.dp, 112.5.dp, 32.dp))
|
||||
rule.setContent { WolfskinTheme {
|
||||
Box(Modifier.requiredSize(960.dp, 540.dp).background(WolfskinTokens.Color.bg)) {
|
||||
Row(Modifier.offset(48.dp, 42.dp), horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
slots.forEach { (name, geometry) ->
|
||||
Box(Modifier.testTag("loading-$name")) {
|
||||
PosterCaptionSkeleton(geometry.first, geometry.second, geometry.third)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} }
|
||||
rule.waitForIdle()
|
||||
val directory = File(System.getProperty("wolfskin.captureDirectory"), "caption-followup").apply { mkdirs() }
|
||||
val file = File(directory, "loading-bands.png")
|
||||
rule.onRoot().captureRoboImage(file.absolutePath)
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
try {
|
||||
for ((name, geometry) in slots) {
|
||||
val bounds = rule.onNodeWithTag("loading-$name").fetchSemanticsNode().boundsInRoot
|
||||
val band = geometry.third.value * 2
|
||||
assertEquals((geometry.second.value + geometry.third.value) * 2, bounds.height, .1f)
|
||||
for (y in (bounds.bottom - band).toInt() until bounds.bottom.toInt()) {
|
||||
for (x in bounds.left.toInt() until bounds.right.toInt()) {
|
||||
assertEquals("$name reserved caption band ($x,$y)", WolfskinTokens.Color.bg.toArgb(), bitmap.getPixel(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally { bitmap.recycle() }
|
||||
}
|
||||
}
|
||||
|
|
@ -11,23 +11,12 @@ import org.junit.runner.RunWith
|
|||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** Caption spacing and live rail changes must preserve poster geometry and retained focus. */
|
||||
/** Live letter-rail changes preserve deep selection and poster geometry. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class LibrarySpacingRegressionTest {
|
||||
@get:Rule val rule = createComposeRule()
|
||||
|
||||
@Test fun partiallyWatchedPosterKeepsItsOriginalProgressInsets() {
|
||||
val title = LibraryTitle("partial", "Partially watched", "movie", progress = .5f)
|
||||
rule.setContent { WolfskinTheme { LibraryPoster(title, false, false, false) } }
|
||||
val poster = rule.onNodeWithTag("poster-partial").fetchSemanticsNode().boundsInRoot
|
||||
val track = rule.onNodeWithTag("poster-progress-partial").fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(256f, poster.width, .1f)
|
||||
assertEquals(220f, track.width, .1f)
|
||||
assertEquals(18f, track.left - poster.left, .1f)
|
||||
assertEquals(18f, poster.right - track.right, .1f)
|
||||
}
|
||||
|
||||
@Test fun crossingRailThresholdKeepsTheSameDeepFocusedPosterVisible() {
|
||||
val titles = List(200) { LibraryTitle("$it", "Title ${it.toString().padStart(3, '0')}", "tv") }
|
||||
val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false; enterDestination("shows") }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue