Preserve native title line boxes and async focus
This commit is contained in:
parent
a8951827ba
commit
43ae716157
9 changed files with 187 additions and 32 deletions
|
|
@ -0,0 +1,88 @@
|
|||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.justinvisser.wolfskin.design.ShellPerson
|
||||
import org.justinvisser.wolfskin.design.ShellState
|
||||
import org.justinvisser.wolfskin.design.TitleItem
|
||||
import org.justinvisser.wolfskin.design.TitlePageData
|
||||
import org.justinvisser.wolfskin.design.TitleReadStatus
|
||||
import org.justinvisser.wolfskin.design.TitleSheet
|
||||
import org.justinvisser.wolfskin.design.TitleTrack
|
||||
import org.justinvisser.wolfskin.design.TitleVersion
|
||||
import org.justinvisser.wolfskin.design.WolfskinTheme
|
||||
import org.justinvisser.wolfskin.tv.read.TitleReadFacade
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.UUID
|
||||
|
||||
/** A delayed service reply exercises the production destination, view-model and Android focus transfer. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class TitleAsyncFocusTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private lateinit var viewModel: WolfskinTitleViewModel
|
||||
|
||||
@After fun dispose() {
|
||||
if (::viewModel.isInitialized) viewModel.viewModelScope.cancel()
|
||||
}
|
||||
|
||||
@Test fun loadingToReadyRestoresRealKeyFocusAndSheetReturn() {
|
||||
val id = UUID.fromString("00000000-0000-0000-0000-000000000012")
|
||||
val ready = CompletableDeferred<TitlePageData>()
|
||||
val facade = mockk<TitleReadFacade>()
|
||||
coEvery { facade.readSaved(id, any()) } returns null
|
||||
coEvery { facade.read(id, any(), any()) } coAnswers { ready.await() }
|
||||
viewModel = WolfskinTitleViewModel(rule.activity, facade, mockk<TitleFixtureProvider>(), mockk<NavigationManager>(relaxed = true))
|
||||
val person = ShellPerson("justin", "Justin", "J", false)
|
||||
val route = titleRoute(id.toString())
|
||||
val shell = ShellState(listOf(person), person.key).apply {
|
||||
pickerVisible = false
|
||||
pickerLaunch = false
|
||||
enterDestination(route)
|
||||
}
|
||||
val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null)
|
||||
rule.setContent { WolfskinTheme { TitleDestination(route, shell, preferences, viewModel = viewModel) } }
|
||||
rule.waitUntil { viewModel.title?.data?.status == TitleReadStatus.Loading }
|
||||
rule.waitForIdle()
|
||||
assertTrue(shell.navFocused)
|
||||
press(Key.DirectionDown)
|
||||
assertTrue("The initial skeleton cannot receive nav Down", shell.navFocused)
|
||||
|
||||
val source = TitleVersion("first", "Original", "1080p", "", "", "MKV", listOf(TitleTrack(3, "English", true)), emptyList())
|
||||
rule.runOnIdle {
|
||||
ready.complete(TitlePageData(TitleItem(id.toString(), "A held film", "movie", held = true), versions = listOf(source, source.copy(id = "second", name = "Alternate"))))
|
||||
}
|
||||
rule.waitUntil { viewModel.title?.data?.status == TitleReadStatus.Ready && !shell.navFocused }
|
||||
press(Key.DirectionRight)
|
||||
val state = requireNotNull(viewModel.title)
|
||||
assertEquals("version", state.options[state.column].id)
|
||||
press(Key.DirectionCenter)
|
||||
assertEquals(TitleSheet.Version, state.sheet)
|
||||
rule.runOnIdle { rule.activity.onBackPressedDispatcher.onBackPressed() }
|
||||
assertNull(state.sheet)
|
||||
press(Key.DirectionRight)
|
||||
assertEquals("tracks", state.options[state.column].id)
|
||||
assertFalse(shell.navFocused)
|
||||
}
|
||||
|
||||
private fun press(key: Key) { rule.onRoot().performKeyInput { pressKey(key) } }
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ import androidx.compose.ui.test.onNodeWithTag
|
|||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import org.json.JSONObject
|
||||
import org.junit.Before
|
||||
|
|
@ -115,6 +117,30 @@ class TitleCaptureTest {
|
|||
.put("boundsPx", org.json.JSONArray(listOf(heading.boundsInRoot.left, heading.boundsInRoot.top, heading.boundsInRoot.right, heading.boundsInRoot.bottom)))
|
||||
File(directory, "st-${number.toString().padStart(3, '0')}-layout.json").writeText(metrics.toString(2))
|
||||
}
|
||||
recordTextMetrics(directory, number)
|
||||
}
|
||||
|
||||
private fun recordTextMetrics(directory: File, number: Int) {
|
||||
val elements = rule.onAllNodes(SemanticsMatcher.keyIsDefined(SemanticsActions.GetTextLayoutResult), useUnmergedTree = true)
|
||||
val records = org.json.JSONArray()
|
||||
elements.fetchSemanticsNodes().forEachIndexed { index, node ->
|
||||
if (node.boundsInRoot.width > 0 && node.boundsInRoot.height > 0) {
|
||||
val layouts = mutableListOf<TextLayoutResult>()
|
||||
elements[index].performSemanticsAction(SemanticsActions.GetTextLayoutResult) { action -> action(layouts) }
|
||||
val layout = layouts.single()
|
||||
records.put(JSONObject().put("text", layout.layoutInput.text.text)
|
||||
.put("fontSizeSp", layout.layoutInput.style.fontSize.value)
|
||||
.put("weight", layout.layoutInput.style.fontWeight?.weight)
|
||||
.put("colourArgb", Integer.toUnsignedString(layout.layoutInput.style.color.toArgb(), 16))
|
||||
.put("lineHeightSp", layout.layoutInput.style.lineHeight.value.takeIf { it.isFinite() } ?: JSONObject.NULL)
|
||||
.put("lineCount", layout.lineCount)
|
||||
.put("boundsPx", org.json.JSONArray(listOf(node.boundsInRoot.left, node.boundsInRoot.top, node.boundsInRoot.right, node.boundsInRoot.bottom)))
|
||||
.put("spans", org.json.JSONArray(layout.layoutInput.text.spanStyles.map { span ->
|
||||
JSONObject().put("start", span.start).put("end", span.end).put("weight", span.item.fontWeight?.weight)
|
||||
})))
|
||||
}
|
||||
}
|
||||
File(directory, "st-${number.toString().padStart(3, '0')}-text.json").writeText(records.toString(2))
|
||||
}
|
||||
|
||||
private fun option(state: TitleState, id: String) { state.focus("header", state.options.indexOfFirst { it.id == id }) }
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import org.justinvisser.wolfskin.design.TitleSurface
|
|||
import org.justinvisser.wolfskin.design.WolfskinTheme
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/** Approved title-entry suffixes replay Android keys through the rendered title and shared shell. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
|
|
@ -289,4 +290,15 @@ class TitleKeysTest {
|
|||
val genre = rule.onNodeWithText("Drama", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
assertEquals(WolfskinTokens.Title.factsGap.value * 2, genre.left - year.right, 1f)
|
||||
}
|
||||
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Test fun mediaInfoRowsPreserveTheirDeclaredLineBoxes() {
|
||||
render()
|
||||
rule.runOnIdle { state.focus("header", state.options.indexOfFirst { it.id == "more" }) }
|
||||
press(Key.DirectionCenter)
|
||||
val container = rule.onNodeWithText("Container", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue