Match title CSS geometry and fix modal focus and subtitle choices
This commit is contained in:
parent
e243894a20
commit
2fdd459037
21 changed files with 297 additions and 74 deletions
|
|
@ -84,5 +84,43 @@ class TitleAsyncFocusTest {
|
|||
assertFalse(shell.navFocused)
|
||||
}
|
||||
|
||||
@Test fun loadingPersonFocusesItsFirstCreditBeforeRealRemoteNavigation() {
|
||||
val id = UUID.fromString("00000000-0000-0000-0000-000000000013")
|
||||
val reply = CompletableDeferred<org.justinvisser.wolfskin.tv.read.PersonRead>()
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.readPersonSaved(id) } returns null
|
||||
coEvery { facade.readPerson(id, 0) } coAnswers { reply.await() }
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), mockk<NavigationManager>(relaxed = true))
|
||||
val viewer = ShellPerson("justin", "Justin", "J", false)
|
||||
val route = personRoute(id.toString())
|
||||
val shell = ShellState(listOf(viewer), viewer.key).apply {
|
||||
pickerVisible = false
|
||||
pickerLaunch = false
|
||||
enterDestination(route)
|
||||
}
|
||||
rule.setContent { WolfskinTheme {
|
||||
TitleDestination(route, shell, UserPreferences(AppPreferences.getDefaultInstance(), null), viewModel = viewModel)
|
||||
} }
|
||||
rule.waitUntil { viewModel.title?.data?.status == TitleReadStatus.Loading }
|
||||
rule.waitForIdle()
|
||||
assertTrue(shell.navFocused)
|
||||
press(Key.DirectionDown)
|
||||
assertTrue(shell.navFocused)
|
||||
rule.runOnIdle {
|
||||
reply.complete(org.justinvisser.wolfskin.tv.read.PersonRead(
|
||||
org.justinvisser.wolfskin.design.TitlePerson(id.toString(), "Actor"),
|
||||
listOf(TitleItem("first-credit", "First credit", "movie"), TitleItem("second-credit", "Second credit", "movie")),
|
||||
))
|
||||
}
|
||||
rule.waitUntil(timeoutMillis = 5000) { viewModel.person != null || viewModel.title?.data?.status == TitleReadStatus.Unavailable }
|
||||
assertNotNull("A real person reply must populate the performer destination", viewModel.person)
|
||||
rule.waitForIdle()
|
||||
assertFalse("The loaded person must transfer nav ownership to its first credit", shell.navFocused)
|
||||
assertEquals(0, viewModel.person!!.index)
|
||||
press(Key.DirectionRight)
|
||||
assertEquals(1, viewModel.person!!.index)
|
||||
assertFalse(shell.navFocused)
|
||||
}
|
||||
|
||||
private fun press(key: Key) { rule.onRoot().performKeyInput { pressKey(key) } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class TitleCaptureTest {
|
|||
File(directory, "st-${number.toString().padStart(3, '0')}-layout.json").writeText(metrics.toString(2))
|
||||
}
|
||||
recordTextMetrics(directory, number)
|
||||
recordElements(directory, number)
|
||||
}
|
||||
|
||||
private fun recordTextMetrics(directory: File, number: Int) {
|
||||
|
|
@ -143,6 +144,20 @@ class TitleCaptureTest {
|
|||
File(directory, "st-${number.toString().padStart(3, '0')}-text.json").writeText(records.toString(2))
|
||||
}
|
||||
|
||||
private fun recordElements(directory: File, number: Int) {
|
||||
val tag = androidx.compose.ui.semantics.SemanticsProperties.TestTag
|
||||
val description = androidx.compose.ui.semantics.SemanticsProperties.ContentDescription
|
||||
val matcher = SemanticsMatcher.keyIsDefined(tag) or SemanticsMatcher.keyIsDefined(description)
|
||||
val records = org.json.JSONArray()
|
||||
rule.onAllNodes(matcher, useUnmergedTree = true).fetchSemanticsNodes().forEach { node ->
|
||||
records.put(JSONObject()
|
||||
.put("tag", if (node.config.contains(tag)) node.config[tag] else JSONObject.NULL)
|
||||
.put("description", if (node.config.contains(description)) org.json.JSONArray(node.config[description]) else JSONObject.NULL)
|
||||
.put("boundsPx", org.json.JSONArray(listOf(node.boundsInRoot.left, node.boundsInRoot.top, node.boundsInRoot.right, node.boundsInRoot.bottom))))
|
||||
}
|
||||
File(directory, "st-${number.toString().padStart(3, '0')}-elements.json").writeText(records.toString(2))
|
||||
}
|
||||
|
||||
private fun option(state: TitleState, id: String) { state.focus("header", state.options.indexOfFirst { it.id == id }) }
|
||||
private fun open(state: TitleState, shell: ShellState, id: String) {
|
||||
option(state, id)
|
||||
|
|
|
|||
|
|
@ -300,5 +300,46 @@ class TitleKeysTest {
|
|||
val size = rule.onNodeWithText("Size", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
val expectedPitch = (WolfskinTokens.Title.infoKey.lineHeight.value + WolfskinTokens.Title.infoRowGap.value) * 2
|
||||
assertEquals("Stacked facts retain the CSS line-box rhythm", expectedPitch, size.top - container.top, 1f)
|
||||
val lastSubtitle = rule.onNodeWithText("Dutch", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
assertEquals("Fourteen stacked lines do not accumulate native pixel rounding", 526.2f, lastSubtitle.top - container.top, 1f)
|
||||
}
|
||||
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Test fun filmHeaderKeepsItsFixedCssStack() {
|
||||
render()
|
||||
assertEquals(358f, rule.onNodeWithTag("title-facts").fetchSemanticsNode().boundsInRoot.top, 1f)
|
||||
assertEquals(410f, rule.onNodeWithTag("title-overview").fetchSemanticsNode().boundsInRoot.top, 1f)
|
||||
assertEquals(553f, rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot.top, 1f)
|
||||
}
|
||||
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Test fun episodeProgressReclaimsItsNegativeMarginBeforeTheActionRow() {
|
||||
render("Andor")
|
||||
assertEquals(603f, rule.onNodeWithTag("title-actions").fetchSemanticsNode().boundsInRoot.top, 1f)
|
||||
}
|
||||
|
||||
@Test fun everyLongTrackChoiceAndDoneRemainInsideTheVisibleSheet() {
|
||||
render(transform = { data ->
|
||||
val source = data.versions.first().copy(
|
||||
audio = List(3) { org.justinvisser.wolfskin.design.TitleTrack(it + 3, "Audio ${it + 1}") },
|
||||
subtitles = List(15) { org.justinvisser.wolfskin.design.TitleTrack(it + 20, "Subtitle ${it + 1}") },
|
||||
)
|
||||
data.copy(versions = listOf(source))
|
||||
})
|
||||
rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "tracks" }) }
|
||||
press(Key.DirectionCenter)
|
||||
val bottom = rule.onRoot().fetchSemanticsNode().boundsInRoot.bottom
|
||||
val labels = (1..3).map { "Audio $it" } + "Off" + (1..15).map { "Subtitle $it" } + "Done"
|
||||
labels.forEachIndexed { index, label ->
|
||||
if (index > 0) press(Key.DirectionDown)
|
||||
rule.waitForIdle()
|
||||
val bounds = rule.onNodeWithText(label, useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
assertTrue("$label must have visible height: $bounds", bounds.height > 20)
|
||||
assertTrue("$label stays above the viewport bottom: $bounds / $bottom", bounds.bottom <= bottom)
|
||||
assertTrue("$label stays below the viewport top: $bounds", bounds.top >= 0)
|
||||
}
|
||||
press(Key.DirectionCenter)
|
||||
assertNull(state.sheet)
|
||||
assertFalse(shell.navFocused)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,4 +166,15 @@ class TitleReadFacadeTest {
|
|||
facade.apply(org.justinvisser.wolfskin.design.TitleAction.Version(data.watchTarget, data.versions.single()))
|
||||
coVerify(exactly = 1) { playback.savePlayVersion(firstId, UUID.fromString("00000000-0000-0000-0000-000000000005")) }
|
||||
}
|
||||
@Test fun subtitleOffDisablesPlaybackRatherThanRestoringThePreferredLanguage() = runTest {
|
||||
coEvery { library.getItem(firstId) } returns successResponse(file(firstId))
|
||||
coEvery { playback.saveTrackSelection(any(), any(), any(), MediaStreamType.SUBTITLE) } returns
|
||||
com.github.damontecres.wholphin.data.model.ItemPlayback(userId = 1, itemId = firstId)
|
||||
val data = facade.read(firstId, null, preferences)
|
||||
facade.apply(org.justinvisser.wolfskin.design.TitleAction.Track(data.watchTarget, data.versions.single(), audio = null, subtitle = -1))
|
||||
coVerify(exactly = 1) { playback.saveTrackSelection(any(), any(), com.github.damontecres.wholphin.data.model.TrackIndex.DISABLED, MediaStreamType.SUBTITLE) }
|
||||
coVerify(exactly = 0) { playback.saveTrackSelection(any(), any(), com.github.damontecres.wholphin.data.model.TrackIndex.UNSPECIFIED, MediaStreamType.SUBTITLE) }
|
||||
facade.apply(org.justinvisser.wolfskin.design.TitleAction.Track(data.watchTarget, data.versions.single(), audio = null, subtitle = 8))
|
||||
coVerify(exactly = 1) { playback.saveTrackSelection(any(), any(), 8, MediaStreamType.SUBTITLE) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue