Restore upstream content focus and deterministic shell typography
This commit is contained in:
parent
9be55c570a
commit
4b89a73908
11 changed files with 251 additions and 52 deletions
|
|
@ -0,0 +1,138 @@
|
|||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import android.app.Application
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toPixelMap
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.test.assertIsFocused
|
||||
import androidx.compose.ui.test.assertIsNotFocused
|
||||
import androidx.compose.ui.test.captureToImage
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropResult
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
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.ShellSurface
|
||||
import org.justinvisser.wolfskin.design.WolfskinTheme
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/** The rollout-off path must preserve native content focus and its backdrop beneath the shell. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class UpstreamContentFocusTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Before fun prepare() {
|
||||
WolfskinScreenPreference.initialize(rule.activity)
|
||||
WolfskinScreenPreference.enabled = false
|
||||
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
|
||||
}
|
||||
|
||||
@After fun restore() { WolfskinScreenPreference.enabled = true }
|
||||
|
||||
private fun shellState() = ShellState(listOf(ShellPerson("justin", "Justin", "J", true)), "justin").apply {
|
||||
pickerVisible = false
|
||||
navFocused = false
|
||||
}
|
||||
|
||||
@Test fun switchOffUpMovesBetweenContentRowsBeforeEnteringNav() {
|
||||
val state = shellState()
|
||||
val first = FocusRequester()
|
||||
val second = FocusRequester()
|
||||
rule.setContent {
|
||||
val upstream = resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, emptyMap())
|
||||
WolfskinTheme {
|
||||
ShellSurface(state, handlesContentKeys = upstream == null) { contentModifier ->
|
||||
Column(contentModifier.padding(start = 100.dp, top = 150.dp), verticalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||
Box(Modifier.size(200.dp, 60.dp).testTag("upstream-first").focusRequester(first).focusable())
|
||||
Box(Modifier.size(200.dp, 60.dp).testTag("upstream-second").focusRequester(second).focusable())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rule.runOnIdle { second.requestFocus() }
|
||||
rule.onNodeWithTag("upstream-second").assertIsFocused()
|
||||
rule.onNodeWithTag("upstream-second").performKeyInput { pressKey(Key.DirectionUp) }
|
||||
rule.onNodeWithTag("upstream-first").assertIsFocused()
|
||||
rule.runOnIdle { assertFalse("Up from the second row must not enter nav", state.navFocused) }
|
||||
rule.onNodeWithTag("upstream-first").performKeyInput { pressKey(Key.DirectionUp) }
|
||||
rule.runOnIdle { assertTrue("Only the first row exits upward to nav", state.navFocused) }
|
||||
rule.onNodeWithTag("upstream-first").assertIsNotFocused()
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { pressKey(Key.DirectionDown) }
|
||||
rule.onNodeWithTag("upstream-first").assertIsFocused()
|
||||
}
|
||||
|
||||
@Test fun switchOffShowsRealUpstreamBackdropAndSwitchOnRestoresOwnedBackground() {
|
||||
val state = shellState()
|
||||
val model = mockk<WolfskinShellViewModel>(relaxed = true)
|
||||
every { model.session } returns WolfskinSession().also { it.state = state }
|
||||
every { model.libraryDestinations } returns emptyMap()
|
||||
// An unavailable saved identity uses the real sign-in placeholder, avoiding any server reads.
|
||||
every { model.signedInPerson } returns "unavailable"
|
||||
every { model.backdropFlow } returns MutableStateFlow(BackdropResult("fixture", null, Color.Red, Color.Red, Color.Red))
|
||||
coEvery { model.load(any(), any()) } returns Unit
|
||||
val server = JellyfinServer(UUID.fromString("00000000-0000-0000-0000-000000000001"), "Fixture", "https://example.invalid", null)
|
||||
val user = JellyfinUser(id = UUID.fromString("00000000-0000-0000-0000-000000000002"), name = "Justin", serverId = server.id, accessToken = null)
|
||||
val defaults = AppPreferences.getDefaultInstance()
|
||||
val preferences = UserPreferences(defaults.toBuilder().setInterfacePreferences(defaults.interfacePreferences.toBuilder().setBackdropStyle(BackdropStyle.BACKDROP_DYNAMIC_COLOR)).build(), null)
|
||||
val navigation = NavigationManager()
|
||||
rule.setContent { WolfskinApp(server, user, navigation, preferences, Modifier.fillMaxSize(), model) }
|
||||
rule.mainClock.advanceTimeBy(1500)
|
||||
rule.waitForIdle()
|
||||
val off = rule.onRoot().captureToImage().toPixelMap()[1700, 900]
|
||||
assertTrue("The real dynamic backdrop must remain visible through native content", off.red > .4f && off.green < .1f && off.blue < .1f)
|
||||
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")))
|
||||
directory.mkdirs()
|
||||
rule.onRoot().captureRoboImage(File(directory, "review-upstream-off.png").absolutePath)
|
||||
rule.runOnIdle { WolfskinScreenPreference.enabled = true }
|
||||
rule.mainClock.advanceTimeBy(1500)
|
||||
rule.waitForIdle()
|
||||
val on = rule.onRoot().captureToImage().toPixelMap()[1700, 900]
|
||||
assertTrue("Owned screens must cover the upstream backdrop", on.red < .02f && on.green < .02f && on.blue < .02f)
|
||||
rule.onRoot().captureRoboImage(File(directory, "review-upstream-on.png").absolutePath)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue