mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
Fix TV detail screen entry focus
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
package hu.bbara.purefin.app.content.episode
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsFocused
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
@@ -16,13 +21,14 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class EpisodeScreenContentTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun episodeScreenContent_showsSeriesContext_andFocusesPlayButton() {
|
||||
fun episodeScreenContent_showsSeriesContext_andMovesFromBackToPlayButton() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
EpisodeScreenContent(
|
||||
@@ -42,8 +48,15 @@ class EpisodeScreenContentTest {
|
||||
composeRule.onNodeWithText("Episode 4").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Overview").assertIsDisplayed()
|
||||
composeRule.onAllNodesWithText("Playback").assertCountEquals(1)
|
||||
composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsDisplayed().assertIsFocused()
|
||||
composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Series").assertIsDisplayed()
|
||||
composeRule.onNodeWithContentDescription("Back")
|
||||
.assertIsDisplayed()
|
||||
.assertIsFocused()
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionDown)
|
||||
}
|
||||
composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsFocused()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package hu.bbara.purefin.app.content.movie
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsFocused
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
@@ -16,13 +20,14 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class MovieScreenContentTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun movieScreenContent_showsTvHeader_andFocusesPlayButton() {
|
||||
fun movieScreenContent_focusesBack_thenMovesToPlayButton() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
MovieScreenContent(
|
||||
@@ -38,8 +43,14 @@ class MovieScreenContentTest {
|
||||
composeRule.onNodeWithText("Blade Runner 2049").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Overview").assertIsDisplayed()
|
||||
composeRule.onAllNodesWithText("Playback").assertCountEquals(1)
|
||||
composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsDisplayed().assertIsFocused()
|
||||
composeRule.onNodeWithContentDescription("Back").assertIsDisplayed()
|
||||
composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsDisplayed()
|
||||
composeRule.onNodeWithContentDescription("Back")
|
||||
.assertIsDisplayed()
|
||||
.assertIsFocused()
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionDown)
|
||||
}
|
||||
composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsFocused()
|
||||
}
|
||||
|
||||
private fun sampleMovie(progress: Double?): Movie {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package hu.bbara.purefin.app.content.series
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsFocused
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
@@ -15,13 +20,14 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.UUID
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class SeriesScreenContentTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun seriesScreenContent_focusesPrimaryAction_whenNextUpExists() {
|
||||
fun seriesScreenContent_movesFromBackToPrimaryAction_whenNextUpExists() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
SeriesScreenContent(
|
||||
@@ -37,13 +43,20 @@ class SeriesScreenContentTest {
|
||||
composeRule.onNodeWithText("Severance").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Overview").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Up Next").assertIsDisplayed()
|
||||
composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsDisplayed().assertIsFocused()
|
||||
composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Season 1").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Good News About Hell").assertIsDisplayed()
|
||||
composeRule.onNodeWithContentDescription("Back")
|
||||
.assertIsDisplayed()
|
||||
.assertIsFocused()
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionDown)
|
||||
}
|
||||
composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsFocused()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun seriesScreenContent_focusesFirstSeason_whenNoPlayableEpisodeExists() {
|
||||
fun seriesScreenContent_movesFromBackToFirstSeason_whenNoPlayableEpisodeExists() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
SeriesScreenContent(
|
||||
@@ -58,7 +71,14 @@ class SeriesScreenContentTest {
|
||||
|
||||
composeRule.onNodeWithText("Overview").assertIsDisplayed()
|
||||
composeRule.onNodeWithText("Library Status").assertIsDisplayed()
|
||||
composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsDisplayed().assertIsFocused()
|
||||
composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsDisplayed()
|
||||
composeRule.onNodeWithContentDescription("Back")
|
||||
.assertIsDisplayed()
|
||||
.assertIsFocused()
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionDown)
|
||||
}
|
||||
composeRule.onNodeWithTag(SeriesFirstSeasonTabTag).assertIsFocused()
|
||||
}
|
||||
|
||||
private fun sampleSeriesWithEpisodes(): Series {
|
||||
|
||||
@@ -53,12 +53,14 @@ internal fun EpisodeTopBar(
|
||||
onBack: () -> Unit,
|
||||
shortcut: EpisodeTopBarShortcut? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
backFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null
|
||||
) {
|
||||
MediaDetailsTopBar(
|
||||
onBack = onBack,
|
||||
shortcut = shortcut?.let { MediaDetailsTopBarShortcut(label = it.label, onClick = it.onClick) },
|
||||
modifier = modifier,
|
||||
backFocusRequester = backFocusRequester,
|
||||
downFocusRequester = downFocusRequester
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,19 +83,22 @@ internal fun EpisodeScreenContent(
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backFocusRequester = remember { FocusRequester() }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(episode.id) {
|
||||
playFocusRequester.requestFocus()
|
||||
backFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = episode.heroImageUrl,
|
||||
resetScrollKey = episode.id,
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
EpisodeTopBar(
|
||||
onBack = onBack,
|
||||
shortcut = topBarShortcut,
|
||||
backFocusRequester = backFocusRequester,
|
||||
downFocusRequester = playFocusRequester,
|
||||
modifier = Modifier.align(Alignment.TopStart)
|
||||
)
|
||||
|
||||
@@ -31,11 +31,13 @@ internal const val MoviePlayButtonTag = "movie-play-button"
|
||||
internal fun MovieTopBar(
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null
|
||||
) {
|
||||
MediaDetailsTopBar(
|
||||
onBack = onBack,
|
||||
modifier = modifier,
|
||||
backFocusRequester = backFocusRequester,
|
||||
downFocusRequester = downFocusRequester
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,18 +52,21 @@ internal fun MovieScreenContent(
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val backFocusRequester = remember { FocusRequester() }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(movie.id) {
|
||||
playFocusRequester.requestFocus()
|
||||
backFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = movie.heroImageUrl,
|
||||
resetScrollKey = movie.id,
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
MovieTopBar(
|
||||
onBack = onBack,
|
||||
backFocusRequester = backFocusRequester,
|
||||
downFocusRequester = playFocusRequester,
|
||||
modifier = Modifier.align(Alignment.TopStart)
|
||||
)
|
||||
|
||||
@@ -74,11 +74,13 @@ internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
|
||||
internal fun SeriesTopBar(
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null
|
||||
) {
|
||||
MediaDetailsTopBar(
|
||||
onBack = onBack,
|
||||
modifier = modifier,
|
||||
backFocusRequester = backFocusRequester,
|
||||
downFocusRequester = downFocusRequester
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,23 +71,22 @@ internal fun SeriesScreenContent(
|
||||
season.episodes.firstOrNull { !it.watched }
|
||||
} ?: series.seasons.firstOrNull()?.episodes?.firstOrNull()
|
||||
}
|
||||
val backFocusRequester = remember { FocusRequester() }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
val firstContentFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(series.id, nextUpEpisode?.id) {
|
||||
if (nextUpEpisode != null) {
|
||||
playFocusRequester.requestFocus()
|
||||
} else {
|
||||
firstContentFocusRequester.requestFocus()
|
||||
}
|
||||
LaunchedEffect(series.id) {
|
||||
backFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
TvMediaDetailScaffold(
|
||||
heroImageUrl = series.heroImageUrl,
|
||||
resetScrollKey = series.id,
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
SeriesTopBar(
|
||||
onBack = onBack,
|
||||
backFocusRequester = backFocusRequester,
|
||||
downFocusRequester = nextUpEpisode?.let { playFocusRequester } ?: firstContentFocusRequester,
|
||||
modifier = Modifier.align(Alignment.TopStart)
|
||||
)
|
||||
|
||||
@@ -14,10 +14,12 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -32,6 +34,7 @@ private val MediaDetailPanelShape = RoundedCornerShape(28.dp)
|
||||
@Composable
|
||||
internal fun TvMediaDetailScaffold(
|
||||
heroImageUrl: String,
|
||||
resetScrollKey: Any,
|
||||
modifier: Modifier = Modifier,
|
||||
heroHeightFraction: Float = 0.48f,
|
||||
topBar: @Composable BoxScope.() -> Unit,
|
||||
@@ -40,6 +43,11 @@ internal fun TvMediaDetailScaffold(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val contentPadding = Modifier.padding(horizontal = MediaDetailHorizontalPadding)
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(resetScrollKey) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -47,6 +55,7 @@ internal fun TvMediaDetailScaffold(
|
||||
.background(scheme.background)
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
@@ -49,6 +50,7 @@ internal fun MediaDetailsTopBar(
|
||||
shortcut: MediaDetailsTopBarShortcut? = null,
|
||||
onCastClick: () -> Unit = {},
|
||||
onMoreClick: () -> Unit = {},
|
||||
backFocusRequester: FocusRequester? = null,
|
||||
downFocusRequester: FocusRequester? = null
|
||||
) {
|
||||
val downModifier = if (downFocusRequester != null) {
|
||||
@@ -56,6 +58,11 @@ internal fun MediaDetailsTopBar(
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
val backModifier = if (backFocusRequester != null) {
|
||||
Modifier.focusRequester(backFocusRequester)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
@@ -70,7 +77,7 @@ internal fun MediaDetailsTopBar(
|
||||
icon = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
onClick = onBack,
|
||||
modifier = downModifier
|
||||
modifier = backModifier.then(downModifier)
|
||||
)
|
||||
if (shortcut != null) {
|
||||
GhostTextButton(
|
||||
|
||||
Reference in New Issue
Block a user