feat(app): expose test tags for Appium

This commit is contained in:
2026-05-21 10:10:08 +02:00
parent 56a2b42434
commit 1274ab25f9
19 changed files with 355 additions and 87 deletions

View File

@@ -22,6 +22,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
@@ -192,7 +194,10 @@ class PurefinActivity : ComponentActivity() {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { navigationManager.pop() }, onBack = { navigationManager.pop() },
modifier = Modifier.fillMaxSize().background(backgroundDark), modifier = Modifier
.fillMaxSize()
.background(backgroundDark)
.semantics { testTagsAsResourceId = true },
transitionSpec = { transitionSpec = {
fadeIn( fadeIn(
animationSpec = tween( animationSpec = tween(
@@ -227,7 +232,9 @@ class PurefinActivity : ComponentActivity() {
} }
} }
} else { } else {
LoginScreen() LoginScreen(
modifier = Modifier.semantics { testTagsAsResourceId = true }
)
} }
} }
} }

View File

@@ -9,6 +9,8 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@Composable @Composable
fun AppBottomBar( fun AppBottomBar(
@@ -16,17 +18,21 @@ fun AppBottomBar(
isOnline: Boolean, isOnline: Boolean,
onTabSelected: (Int) -> Unit onTabSelected: (Int) -> Unit
) { ) {
NavigationBar { NavigationBar(
modifier = Modifier.testTag(BottomNavigationTag)
) {
if (isOnline) { if (isOnline) {
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 0, selected = selectedTab == 0,
onClick = { onTabSelected(0) }, onClick = { onTabSelected(0) },
modifier = Modifier.testTag(BottomNavigationHomeTag),
icon = { Icon(Icons.Outlined.Home, contentDescription = "Home") }, icon = { Icon(Icons.Outlined.Home, contentDescription = "Home") },
label = { Text("Home") } label = { Text("Home") }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == 1, selected = selectedTab == 1,
onClick = { onTabSelected(1) }, onClick = { onTabSelected(1) },
modifier = Modifier.testTag(BottomNavigationLibrariesTag),
icon = { Icon(Icons.Outlined.Collections, contentDescription = "Libraries") }, icon = { Icon(Icons.Outlined.Collections, contentDescription = "Libraries") },
label = { Text("Libraries") } label = { Text("Libraries") }
) )
@@ -34,8 +40,14 @@ fun AppBottomBar(
NavigationBarItem( NavigationBarItem(
selected = !isOnline || selectedTab == 2, selected = !isOnline || selectedTab == 2,
onClick = { onTabSelected(2) }, onClick = { onTabSelected(2) },
modifier = Modifier.testTag(BottomNavigationDownloadsTag),
icon = { Icon(Icons.Outlined.Download, contentDescription = "Downloads") }, icon = { Icon(Icons.Outlined.Download, contentDescription = "Downloads") },
label = { Text("Downloads") } label = { Text("Downloads") }
) )
} }
} }
internal const val BottomNavigationTag = "bottom-navigation"
internal const val BottomNavigationHomeTag = "bottom-navigation-home"
internal const val BottomNavigationLibrariesTag = "bottom-navigation-libraries"
internal const val BottomNavigationDownloadsTag = "bottom-navigation-downloads"

View File

@@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -33,9 +34,10 @@ import hu.bbara.purefin.core.feature.downloads.ActiveDownloadItem
internal fun DownloadingItemRow( internal fun DownloadingItemRow(
item: ActiveDownloadItem, item: ActiveDownloadItem,
onCancel: (String) -> Unit, onCancel: (String) -> Unit,
modifier: Modifier = Modifier
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
@@ -91,7 +93,10 @@ internal fun DownloadingItemRow(
) )
} }
} }
IconButton(onClick = { onCancel(item.contentId) }) { IconButton(
onClick = { onCancel(item.contentId) },
modifier = Modifier.testTag("$DownloadsCancelButtonTagPrefix${item.contentId}")
) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = "Cancel download", contentDescription = "Cancel download",
@@ -101,3 +106,5 @@ internal fun DownloadingItemRow(
} }
} }
} }
internal const val DownloadsCancelButtonTagPrefix = "downloads-cancel-button-"

View File

@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -38,7 +39,9 @@ fun DownloadsContent(
if (isEmpty) { if (isEmpty) {
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier
.fillMaxSize()
.testTag(DownloadsEmptyStateTag),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -62,7 +65,9 @@ fun DownloadsContent(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.background(MaterialTheme.colorScheme.background) modifier = modifier
.testTag(DownloadsGridTag)
.background(MaterialTheme.colorScheme.background)
) { ) {
if (activeDownloads.value.isNotEmpty()) { if (activeDownloads.value.isNotEmpty()) {
item(key = "downloading-header", span = { GridItemSpan(maxLineSpan) }) { item(key = "downloading-header", span = { GridItemSpan(maxLineSpan) }) {
@@ -80,7 +85,8 @@ fun DownloadsContent(
) { item -> ) { item ->
DownloadingItemRow( DownloadingItemRow(
item = item, item = item,
onCancel = { viewModel.cancelDownload(it) } onCancel = { viewModel.cancelDownload(it) },
modifier = Modifier.testTag("$DownloadsActiveItemTagPrefix${item.contentId}")
) )
} }
if (downloads.value.isNotEmpty()) { if (downloads.value.isNotEmpty()) {
@@ -101,7 +107,13 @@ fun DownloadsContent(
onMovieSelected = viewModel::onMovieSelected, onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected, onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = { _, _, _ -> }, onEpisodeSelected = { _, _, _ -> },
modifier = Modifier.testTag("$DownloadsItemTagPrefix${item.id}")
) )
} }
} }
} }
internal const val DownloadsEmptyStateTag = "downloads-empty-state"
internal const val DownloadsGridTag = "downloads-grid"
internal const val DownloadsActiveItemTagPrefix = "downloads-active-item-"
internal const val DownloadsItemTagPrefix = "downloads-item-"

View File

@@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -71,6 +72,7 @@ internal fun EpisodeTopBar(
), ),
modifier = Modifier modifier = Modifier
.height(52.dp) .height(52.dp)
.testTag(EpisodeSeriesButtonTag)
.clip(CircleShape) .clip(CircleShape)
) { ) {
Text( Text(
@@ -123,7 +125,9 @@ internal fun EpisodeDetails(
text = mediaPlayButtonText(episode.progress, episode.watched), text = mediaPlayButtonText(episode.progress, episode.watched),
progress = mediaPlaybackProgress(episode.progress), progress = mediaPlaybackProgress(episode.progress),
onClick = playAction, onClick = playAction,
modifier = Modifier.sizeIn(maxWidth = 200.dp) modifier = Modifier
.sizeIn(maxWidth = 200.dp)
.testTag(EpisodePlayButtonTag)
) )
VerticalDivider( VerticalDivider(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
@@ -143,6 +147,7 @@ internal fun EpisodeDetails(
is DownloadState.Failed -> Icons.Outlined.Download is DownloadState.Failed -> Icons.Outlined.Download
}, },
height = 48.dp, height = 48.dp,
modifier = Modifier.testTag(EpisodeDownloadButtonTag),
onClick = onDownloadClick onClick = onDownloadClick
) )
} }
@@ -168,3 +173,7 @@ internal fun EpisodeDetails(
// ) // )
} }
} }
internal const val EpisodeSeriesButtonTag = "episode-series-button"
internal const val EpisodePlayButtonTag = "episode-play-button"
internal const val EpisodeDownloadButtonTag = "episode-download-button"

View File

@@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.LibraryUiModel import hu.bbara.purefin.core.model.LibraryUiModel
import hu.bbara.purefin.core.model.MediaUiModel import hu.bbara.purefin.core.model.MediaUiModel
@@ -80,7 +81,9 @@ fun HomeContent(
PullToRefreshBox( PullToRefreshBox(
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
onRefresh = onRefresh, onRefresh = onRefresh,
modifier = modifier.fillMaxSize() modifier = modifier
.fillMaxSize()
.testTag(HomeContentTag)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -89,7 +92,9 @@ fun HomeContent(
) { ) {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.testTag(HomeContentViewportTag),
contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp), contentPadding = PaddingValues(top = 16.dp, bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp) verticalArrangement = Arrangement.spacedBy(24.dp)
) { ) {
@@ -97,7 +102,8 @@ fun HomeContent(
item(key = "featured") { item(key = "featured") {
SuggestionsSection( SuggestionsSection(
items = suggestions, items = suggestions,
onItemOpen = { item -> onMediaSelected(item) } onItemOpen = { item -> onMediaSelected(item) },
modifier = Modifier.testTag(HomeFeaturedSectionTag)
) )
} }
} }
@@ -107,7 +113,8 @@ fun HomeContent(
ContinueWatchingSection( ContinueWatchingSection(
items = continueWatching, items = continueWatching,
onMarkAsWatched = onMarkAsWatched, onMarkAsWatched = onMarkAsWatched,
onMediaSelected = onMediaSelected onMediaSelected = onMediaSelected,
modifier = Modifier.testTag(HomeContinueWatchingSectionTag)
) )
} }
} }
@@ -116,7 +123,8 @@ fun HomeContent(
item(key = "next-up") { item(key = "next-up") {
NextUpSection( NextUpSection(
items = nextUp, items = nextUp,
onMediaSelected = onMediaSelected onMediaSelected = onMediaSelected,
modifier = Modifier.testTag(HomeNextUpSectionTag)
) )
} }
} }
@@ -129,7 +137,8 @@ fun HomeContent(
library = library, library = library,
items = libraryContent[library.id].orEmpty(), items = libraryContent[library.id].orEmpty(),
onLibrarySelected = onLibrarySelected, onLibrarySelected = onLibrarySelected,
onMediaSelected = onMediaSelected onMediaSelected = onMediaSelected,
modifier = Modifier.testTag("$HomeLibrarySectionTagPrefix${library.id}")
) )
} }
@@ -137,7 +146,8 @@ fun HomeContent(
item(key = "empty-state") { item(key = "empty-state") {
HomeEmptyState( HomeEmptyState(
onRefresh = onRefresh, onRefresh = onRefresh,
onBrowseLibrariesClick = onBrowseLibrariesClick onBrowseLibrariesClick = onBrowseLibrariesClick,
modifier = Modifier.testTag(HomeEmptyStateTag)
) )
} }
} }
@@ -145,3 +155,11 @@ fun HomeContent(
} }
} }
} }
internal const val HomeContentTag = "home-content"
internal const val HomeContentViewportTag = "home-content-viewport"
internal const val HomeFeaturedSectionTag = "home-section-featured"
internal const val HomeContinueWatchingSectionTag = "home-section-continue-watching"
internal const val HomeNextUpSectionTag = "home-section-next-up"
internal const val HomeLibrarySectionTagPrefix = "home-section-library-"
internal const val HomeEmptyStateTag = "home-empty-state"

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
@@ -41,7 +42,9 @@ fun ContinueWatchingSection(
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.testTag(HomeContinueWatchingRowTag),
state = listState state = listState
) { ) {
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
@@ -49,9 +52,13 @@ fun ContinueWatchingSection(
item = item, item = item,
sharedBoundsKey = homeMediaSharedBoundsKey("continue-$index", item.id), sharedBoundsKey = homeMediaSharedBoundsKey("continue-$index", item.id),
onMediaSelected = onMediaSelected, onMediaSelected = onMediaSelected,
onMarkAsWatched = onMarkAsWatched onMarkAsWatched = onMarkAsWatched,
modifier = Modifier.testTag("$HomeContinueWatchingItemTagPrefix$index")
) )
} }
} }
} }
} }
internal const val HomeContinueWatchingRowTag = "home-continue-watching-row"
internal const val HomeContinueWatchingItemTagPrefix = "home-continue-watching-item-"

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
import hu.bbara.purefin.core.model.MediaUiModel import hu.bbara.purefin.core.model.MediaUiModel
@@ -48,13 +49,16 @@ fun SuggestionsSection(
pageSize = PageSize.Fixed(320.dp), pageSize = PageSize.Fixed(320.dp),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
pageSpacing = 16.dp, pageSpacing = 16.dp,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.testTag(HomeFeaturedPagerTag)
) { page -> ) { page ->
val item = items[page] val item = items[page]
SuggestionCard( SuggestionCard(
item = item, item = item,
sharedBoundsKey = homeMediaSharedBoundsKey("suggestion-$page", item.id), sharedBoundsKey = homeMediaSharedBoundsKey("suggestion-$page", item.id),
onClick = { onItemOpen(item) } onClick = { onItemOpen(item) },
modifier = Modifier.testTag("$HomeFeaturedItemTagPrefix$page")
) )
} }
if (items.size > 1) { if (items.size > 1) {
@@ -86,3 +90,6 @@ fun SuggestionsSection(
} }
} }
} }
internal const val HomeFeaturedPagerTag = "home-featured-pager"
internal const val HomeFeaturedItemTagPrefix = "home-featured-item-"

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
@@ -47,7 +48,9 @@ fun LibraryPosterSection(
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.testTag("$HomeLibraryRowTagPrefix${library.id}"),
state = listState state = listState
) { ) {
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
@@ -57,9 +60,13 @@ fun LibraryPosterSection(
origin = "library-${library.id}-$index", origin = "library-${library.id}-$index",
mediaId = item.id mediaId = item.id
), ),
onMediaSelected = onMediaSelected onMediaSelected = onMediaSelected,
modifier = Modifier.testTag("$HomeLibraryItemTagPrefix${library.id}-$index")
) )
} }
} }
} }
} }
internal const val HomeLibraryRowTagPrefix = "home-library-row-"
internal const val HomeLibraryItemTagPrefix = "home-library-item-"

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsKey
@@ -40,16 +41,22 @@ fun NextUpSection(
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.testTag(HomeNextUpRowTag),
state = listState state = listState
) { ) {
itemsIndexed(items = items, key = { _, item -> item.id }) { index, item -> itemsIndexed(items = items, key = { _, item -> item.id }) { index, item ->
NextUpCard( NextUpCard(
uiModel = item, uiModel = item,
sharedBoundsKey = homeMediaSharedBoundsKey("next-up-$index", item.id), sharedBoundsKey = homeMediaSharedBoundsKey("next-up-$index", item.id),
onMediaSelected = onMediaSelected onMediaSelected = onMediaSelected,
modifier = Modifier.testTag("$HomeNextUpItemTagPrefix$index")
) )
} }
} }
} }
} }
internal const val HomeNextUpRowTag = "home-next-up-row"
internal const val HomeNextUpItemTagPrefix = "home-next-up-item-"

View File

@@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -147,6 +148,7 @@ private fun SearchScreenContent(
) }, ) },
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.testTag(SearchScreenTag)
.background(scheme.background) .background(scheme.background)
) { innerPadding -> ) { innerPadding ->
Column( Column(
@@ -261,7 +263,10 @@ private fun SearchField(
}, },
trailingIcon = { trailingIcon = {
if (query.isNotBlank()) { if (query.isNotBlank()) {
IconButton(onClick = { onQueryChange("") }) { IconButton(
onClick = { onQueryChange("") },
modifier = Modifier.testTag(SearchClearButtonTag)
) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Outlined.Close,
contentDescription = "Clear search", contentDescription = "Clear search",
@@ -286,6 +291,7 @@ private fun SearchField(
), ),
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(SearchQueryFieldTag)
.height(68.dp) .height(68.dp)
) )
} }
@@ -298,18 +304,23 @@ private fun SearchResultsGrid(
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.fillMaxWidth() modifier = modifier
.fillMaxWidth()
.testTag(SearchResultsGridTag)
) { ) {
results.chunked(2).forEach { rowItems -> results.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
rowItems.forEach { item -> rowItems.forEachIndexed { columnIndex, item ->
val itemIndex = rowIndex * 2 + columnIndex
SearchResultCard( SearchResultCard(
item = item, item = item,
onClick = { onResultClick(item) }, onClick = { onResultClick(item) },
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.testTag("$SearchResultItemTagPrefix$itemIndex")
) )
} }
if (rowItems.size == 1) { if (rowItems.size == 1) {
@@ -367,6 +378,7 @@ private fun GenreSelector(
FilterChip( FilterChip(
selected = false, selected = false,
onClick = { showGenres = true }, onClick = { showGenres = true },
modifier = Modifier.testTag(SearchGenreSelectorTag),
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Tune, imageVector = Icons.Outlined.Tune,
@@ -388,11 +400,12 @@ private fun GenreSelector(
) )
} }
) )
visibleGenres.forEach { genre -> visibleGenres.forEachIndexed { index, genre ->
GenreChip( GenreChip(
genre = genre, genre = genre,
selected = genre.name in selectedGenreNames, selected = genre.name in selectedGenreNames,
onClick = { onGenreSelected(genre) } onClick = { onGenreSelected(genre) },
modifier = Modifier.testTag("$SearchGenreChipTagPrefix$index")
) )
} }
} }
@@ -400,6 +413,7 @@ private fun GenreSelector(
if (showGenres) { if (showGenres) {
AlertDialog( AlertDialog(
onDismissRequest = { showGenres = false }, onDismissRequest = { showGenres = false },
modifier = Modifier.testTag(SearchGenreDialogTag),
title = { title = {
Text(text = "Select genres") Text(text = "Select genres")
}, },
@@ -412,11 +426,12 @@ private fun GenreSelector(
.heightIn(max = 360.dp) .heightIn(max = 360.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
genres.forEach { genre -> genres.forEachIndexed { index, genre ->
GenreChip( GenreChip(
genre = genre, genre = genre,
selected = genre.name in selectedGenreNames, selected = genre.name in selectedGenreNames,
onClick = { onGenreSelected(genre) } onClick = { onGenreSelected(genre) },
modifier = Modifier.testTag("$SearchGenreDialogChipTagPrefix$index")
) )
} }
} }
@@ -514,3 +529,13 @@ private val previewSearchResults = listOf(
type = MediaKind.MOVIE type = MediaKind.MOVIE
) )
) )
internal const val SearchScreenTag = "search-screen"
internal const val SearchQueryFieldTag = "search-query-field"
internal const val SearchClearButtonTag = "search-clear-button"
internal const val SearchResultsGridTag = "search-results-grid"
internal const val SearchResultItemTagPrefix = "search-result-item-"
internal const val SearchGenreSelectorTag = "search-genre-selector"
internal const val SearchGenreChipTagPrefix = "search-genre-chip-"
internal const val SearchGenreDialogTag = "search-genre-dialog"
internal const val SearchGenreDialogChipTagPrefix = "search-genre-dialog-chip-"

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.LibraryUiModel import hu.bbara.purefin.core.model.LibraryUiModel
@@ -17,7 +18,7 @@ fun LibrariesContent(
onLibrarySelected: (LibraryUiModel) -> Unit, onLibrarySelected: (LibraryUiModel) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BoxWithConstraints(modifier = modifier) { BoxWithConstraints(modifier = modifier.testTag(LibrariesOverviewGridTag)) {
val minCellSize = if (maxWidth >= 600.dp) 220.dp else 160.dp val minCellSize = if (maxWidth >= 600.dp) 220.dp else 160.dp
LazyVerticalGrid( LazyVerticalGrid(
@@ -29,9 +30,13 @@ fun LibrariesContent(
items(items, key = { it.id }) { item -> items(items, key = { it.id }) { item ->
LibraryListItem( LibraryListItem(
uiModel = item, uiModel = item,
onClick = { onLibrarySelected(item) } onClick = { onLibrarySelected(item) },
modifier = Modifier.testTag("$LibrariesOverviewItemTagPrefix${item.id}")
) )
} }
} }
} }
} }
internal const val LibrariesOverviewGridTag = "libraries-overview-grid"
internal const val LibrariesOverviewItemTagPrefix = "libraries-overview-item-"

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.core.feature.browse.library.LibraryViewModel import hu.bbara.purefin.core.feature.browse.library.LibraryViewModel
@@ -42,7 +43,7 @@ fun LibraryScreen(
val libraryItems = viewModel.contents.collectAsState() val libraryItems = viewModel.contents.collectAsState()
Scaffold( Scaffold(
modifier = modifier, modifier = modifier.testTag(LibraryScreenTag),
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
topBar = { topBar = {
@@ -68,7 +69,11 @@ internal fun LibraryPosterGrid(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: LibraryViewModel = hiltViewModel() viewModel: LibraryViewModel = hiltViewModel()
) { ) {
BoxWithConstraints(modifier = modifier.background(MaterialTheme.colorScheme.background)) { BoxWithConstraints(
modifier = modifier
.testTag(LibraryPosterGridTag)
.background(MaterialTheme.colorScheme.background)
) {
val minCellSize = if (maxWidth >= 600.dp) 220.dp else 120.dp val minCellSize = if (maxWidth >= 600.dp) 220.dp else 120.dp
LazyVerticalGrid( LazyVerticalGrid(
@@ -88,7 +93,8 @@ internal fun LibraryPosterGrid(
is SeriesUiModel -> viewModel.onSeriesSelected(item.id) is SeriesUiModel -> viewModel.onSeriesSelected(item.id)
is EpisodeUiModel -> Unit is EpisodeUiModel -> Unit
} }
} },
modifier = Modifier.testTag("$LibraryPosterItemTagPrefix${item.id}")
) { ) {
when (item) { when (item) {
is MovieUiModel, is EpisodeUiModel -> { is MovieUiModel, is EpisodeUiModel -> {
@@ -107,3 +113,7 @@ internal fun LibraryPosterGrid(
} }
} }
} }
internal const val LibraryScreenTag = "library-screen"
internal const val LibraryPosterGridTag = "library-poster-grid"
internal const val LibraryPosterItemTagPrefix = "library-poster-item-"

View File

@@ -22,6 +22,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -47,6 +48,7 @@ fun LoginContent(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.testTag(LoginContentTag)
.background(scheme.background) .background(scheme.background)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp, vertical = 20.dp), .padding(horizontal = 24.dp, vertical = 20.dp),
@@ -84,6 +86,7 @@ fun LoginContent(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(LoginErrorMessageTag)
.background( .background(
scheme.errorContainer, scheme.errorContainer,
RoundedCornerShape(12.dp) RoundedCornerShape(12.dp)
@@ -121,6 +124,7 @@ private fun ServerSearchContent(
modifier = Modifier.padding(top = 2.dp, bottom = 16.dp).fillMaxWidth() modifier = Modifier.padding(top = 2.dp, bottom = 16.dp).fillMaxWidth()
) )
Column(modifier = Modifier.testTag(LoginServerUrlFieldTag)) {
PurefinComplexTextField( PurefinComplexTextField(
label = "Server URL", label = "Server URL",
value = state.serverUrl, value = state.serverUrl,
@@ -128,6 +132,7 @@ private fun ServerSearchContent(
placeholder = "http://192.168.1.100:8096", placeholder = "http://192.168.1.100:8096",
leadingIcon = Icons.Default.Storage leadingIcon = Icons.Default.Storage
) )
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -137,6 +142,7 @@ private fun ServerSearchContent(
enabled = !state.isSearching, enabled = !state.isSearching,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(LoginFindServerButtonTag)
.height(48.dp) .height(48.dp)
) )
@@ -149,10 +155,12 @@ private fun ServerSearchContent(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
state.discoveredServers.forEach { server -> state.discoveredServers.forEachIndexed { index, server ->
TextButton( TextButton(
onClick = { callbacks.onDiscoveredServerClick(server) }, onClick = { callbacks.onDiscoveredServerClick(server) },
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.testTag("$LoginDiscoveredServerTagPrefix$index")
) { ) {
Text( Text(
text = server.name?.let { "$it\n${server.address}" } ?: server.address, text = server.name?.let { "$it\n${server.address}" } ?: server.address,
@@ -186,7 +194,9 @@ private fun LoginPhaseContent(
) )
TextButton( TextButton(
onClick = callbacks.onChangeServer, onClick = callbacks.onChangeServer,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.testTag(LoginChangeServerButtonTag)
) { ) {
Text("Change server") Text("Change server")
} }
@@ -199,7 +209,9 @@ private fun LoginPhaseContent(
color = scheme.primary, color = scheme.primary,
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.testTag(LoginQuickConnectCodeTag)
) )
Text( Text(
text = "Approve this code in another Jellyfin client.", text = "Approve this code in another Jellyfin client.",
@@ -209,14 +221,20 @@ private fun LoginPhaseContent(
modifier = Modifier.fillMaxWidth().padding(top = 4.dp) modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
TextButton(onClick = callbacks.onCancelQuickConnect) { TextButton(
onClick = callbacks.onCancelQuickConnect,
modifier = Modifier.testTag(LoginQuickConnectCancelButtonTag)
) {
Text("Cancel Quick Connect") Text("Cancel Quick Connect")
} }
} ?: PurefinTextButton( } ?: PurefinTextButton(
content = { Text("Quick Connect") }, content = { Text("Quick Connect") },
onClick = callbacks.onQuickConnect, onClick = callbacks.onQuickConnect,
enabled = !state.isQuickConnecting, enabled = !state.isQuickConnecting,
modifier = Modifier.fillMaxWidth().height(48.dp) modifier = Modifier
.fillMaxWidth()
.testTag(LoginQuickConnectButtonTag)
.height(48.dp)
) )
} else { } else {
Text( Text(
@@ -238,6 +256,7 @@ private fun LoginPhaseContent(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Column(modifier = Modifier.testTag(LoginUsernameFieldTag)) {
PurefinComplexTextField( PurefinComplexTextField(
label = "Username", label = "Username",
value = state.username, value = state.username,
@@ -245,9 +264,11 @@ private fun LoginPhaseContent(
placeholder = "Enter your username", placeholder = "Enter your username",
leadingIcon = Icons.Default.Person leadingIcon = Icons.Default.Person
) )
}
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Column(modifier = Modifier.testTag(LoginPasswordFieldTag)) {
PurefinPasswordField( PurefinPasswordField(
label = "Password", label = "Password",
value = state.password, value = state.password,
@@ -255,6 +276,7 @@ private fun LoginPhaseContent(
placeholder = "Enter your password", placeholder = "Enter your password",
leadingIcon = Icons.Default.Lock, leadingIcon = Icons.Default.Lock,
) )
}
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@@ -264,6 +286,20 @@ private fun LoginPhaseContent(
enabled = !state.isQuickConnecting, enabled = !state.isQuickConnecting,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(LoginConnectButtonTag)
.height(48.dp) .height(48.dp)
) )
} }
internal const val LoginContentTag = "login-content"
internal const val LoginFindServerButtonTag = "login-find-server-button"
internal const val LoginServerUrlFieldTag = "login-server-url-field"
internal const val LoginDiscoveredServerTagPrefix = "login-discovered-server-"
internal const val LoginChangeServerButtonTag = "login-change-server-button"
internal const val LoginUsernameFieldTag = "login-username-field"
internal const val LoginPasswordFieldTag = "login-password-field"
internal const val LoginQuickConnectButtonTag = "login-quick-connect-button"
internal const val LoginQuickConnectCodeTag = "login-quick-connect-code"
internal const val LoginQuickConnectCancelButtonTag = "login-quick-connect-cancel-button"
internal const val LoginConnectButtonTag = "login-connect-button"
internal const val LoginErrorMessageTag = "login-error-message"

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -85,7 +86,9 @@ internal fun MovieDetails(
text = mediaPlayButtonText(movie.progress, movie.watched), text = mediaPlayButtonText(movie.progress, movie.watched),
progress = mediaPlaybackProgress(movie.progress), progress = mediaPlaybackProgress(movie.progress),
onClick = playAction, onClick = playAction,
modifier = Modifier.sizeIn(maxWidth = 200.dp) modifier = Modifier
.sizeIn(maxWidth = 200.dp)
.testTag(MoviePlayButtonTag)
) )
VerticalDivider( VerticalDivider(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
@@ -105,6 +108,7 @@ internal fun MovieDetails(
is DownloadState.Failed -> Icons.Outlined.Download is DownloadState.Failed -> Icons.Outlined.Download
}, },
height = 48.dp, height = 48.dp,
modifier = Modifier.testTag(MovieDownloadButtonTag),
onClick = onDownloadClick onClick = onDownloadClick
) )
} }
@@ -130,3 +134,6 @@ internal fun MovieDetails(
// ) // )
} }
} }
internal const val MoviePlayButtonTag = "movie-play-button"
internal const val MovieDownloadButtonTag = "movie-download-button"

View File

@@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.player.model.PlayerUiState import hu.bbara.purefin.core.player.model.PlayerUiState
@@ -70,6 +71,7 @@ fun PlayerControlsOverlay(
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.testTag(PlayerControlsOverlayTag)
.background( .background(
Brush.verticalGradient( Brush.verticalGradient(
listOf( listOf(
@@ -149,6 +151,7 @@ private fun TopBar(
GhostIconButton( GhostIconButton(
icon = Icons.Outlined.PlaylistPlay, icon = Icons.Outlined.PlaylistPlay,
contentDescription = "Queue", contentDescription = "Queue",
modifier = Modifier.testTag(PlayerQueueButtonTag),
onClick = onOpenQueue onClick = onOpenQueue
) )
GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = onCast) GhostIconButton(icon = Icons.Outlined.Cast, contentDescription = "Cast", onClick = onCast)
@@ -192,7 +195,8 @@ private fun BottomSection(
adMarkers = uiState.ads, adMarkers = uiState.ads,
onSeek = onScrub, onSeek = onScrub,
onScrubStarted = onScrubStart, onScrubStarted = onScrubStart,
onScrubFinished = onScrubFinished onScrubFinished = onScrubFinished,
modifier = Modifier.testTag(PlayerSeekBarTag)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Box( Box(
@@ -209,30 +213,35 @@ private fun BottomSection(
icon = Icons.Outlined.SkipPrevious, icon = Icons.Outlined.SkipPrevious,
contentDescription = "Previous", contentDescription = "Previous",
onClick = onPrevious, onClick = onPrevious,
modifier = Modifier.testTag(PlayerPreviousButtonTag),
size = 64 size = 64
) )
PurefinIconButton( PurefinIconButton(
icon = Icons.Outlined.Replay10, icon = Icons.Outlined.Replay10,
contentDescription = "Seek backward", contentDescription = "Seek backward",
onClick = onSeekBackward, onClick = onSeekBackward,
modifier = Modifier.testTag(PlayerSeekBackwardButtonTag),
size = 64 size = 64
) )
PurefinIconButton( PurefinIconButton(
icon = if (uiState.isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow, icon = if (uiState.isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow,
contentDescription = "Play/Pause", contentDescription = "Play/Pause",
onClick = onPlayPause, onClick = onPlayPause,
modifier = Modifier.testTag(PlayerPlayPauseButtonTag),
size = 64 size = 64
) )
PurefinIconButton( PurefinIconButton(
icon = Icons.Outlined.Forward30, icon = Icons.Outlined.Forward30,
contentDescription = "Seek forward", contentDescription = "Seek forward",
onClick = onSeekForward, onClick = onSeekForward,
modifier = Modifier.testTag(PlayerSeekForwardButtonTag),
size = 64 size = 64
) )
PurefinIconButton( PurefinIconButton(
icon = Icons.Outlined.SkipNext, icon = Icons.Outlined.SkipNext,
contentDescription = "Next", contentDescription = "Next",
onClick = onNext, onClick = onNext,
modifier = Modifier.testTag(PlayerNextButtonTag),
size = 64 size = 64
) )
} }
@@ -263,7 +272,9 @@ private fun BottomSection(
if (uiState.activeSkippableSegmentEndMs != null) { if (uiState.activeSkippableSegmentEndMs != null) {
SkipSegmentButton( SkipSegmentButton(
onClick = onSkipSegment, onClick = onSkipSegment,
modifier = Modifier.align(Alignment.CenterEnd) modifier = Modifier
.align(Alignment.CenterEnd)
.testTag(PlayerSkipSegmentButtonTag)
) )
} }
} }
@@ -271,6 +282,16 @@ private fun BottomSection(
} }
} }
internal const val PlayerControlsOverlayTag = "player-controls-overlay"
internal const val PlayerQueueButtonTag = "player_queue_button"
internal const val PlayerSeekBarTag = "player_seek_bar"
internal const val PlayerPreviousButtonTag = "player_previous_button"
internal const val PlayerSeekBackwardButtonTag = "player_seek_backward_button"
internal const val PlayerPlayPauseButtonTag = "player_play_pause_button"
internal const val PlayerSeekForwardButtonTag = "player_seek_forward_button"
internal const val PlayerNextButtonTag = "player_next_button"
internal const val PlayerSkipSegmentButtonTag = "player_skip_segment_button"
@Composable @Composable
fun SkipSegmentButton( fun SkipSegmentButton(
onClick: () -> Unit, onClick: () -> Unit,

View File

@@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -40,7 +41,12 @@ fun PlayerQueuePanel(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Box(
modifier = modifier
.fillMaxWidth()
.testTag(PlayerPlaylistPanelTag),
contentAlignment = Alignment.CenterEnd
) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
@@ -78,13 +84,23 @@ fun PlayerQueuePanel(
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut() exit = fadeOut()
) { ) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) { LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.testTag(PlayerPlaylistRowTag)
) {
items(uiState.queue, key = { item -> item.id }) { item -> items(uiState.queue, key = { item -> item.id }) { item ->
QueueRow( QueueRow(
title = item.title, title = item.title,
artworkUrl = item.artworkUrl, artworkUrl = item.artworkUrl,
isCurrent = item.isCurrent, isCurrent = item.isCurrent,
onClick = { onSelect(item.id) } onClick = { onSelect(item.id) },
modifier = Modifier.testTag(
if (item.isCurrent) {
PlayerPlaylistCurrentItemTag
} else {
"${PlayerPlaylistItemTagPrefix}${item.id}"
}
)
) )
} }
} }
@@ -106,11 +122,12 @@ private fun QueueRow(
subtitle: String? = null, subtitle: String? = null,
artworkUrl: String?, artworkUrl: String?,
isCurrent: Boolean, isCurrent: Boolean,
onClick: () -> Unit onClick: () -> Unit,
modifier: Modifier = Modifier
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
Row( Row(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background( .background(
@@ -160,3 +177,8 @@ private fun QueueRow(
} }
} }
} }
internal const val PlayerPlaylistPanelTag = "player_playlist_panel"
internal const val PlayerPlaylistRowTag = "player_playlist_row"
internal const val PlayerPlaylistCurrentItemTag = "player_playlist_current_item"
internal const val PlayerPlaylistItemTagPrefix = "player_playlist_item_"

View File

@@ -32,6 +32,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -63,7 +64,7 @@ fun QualitySelectionButton(
) )
} }
}, },
modifier = modifier modifier = modifier.testTag(PlayerQualityButtonTag)
) )
} }
@@ -92,7 +93,7 @@ fun AudioSelectionButton(
) )
} }
}, },
modifier = modifier modifier = modifier.testTag(PlayerAudioButtonTag)
) )
} }
@@ -121,7 +122,7 @@ fun SubtitlesSelectionButton(
) )
} }
}, },
modifier = modifier modifier = modifier.testTag(PlayerSubtitlesButtonTag)
) )
} }
@@ -139,6 +140,7 @@ private fun TrackSelectionPanel(
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(PlayerTrackPanelTag)
.fillMaxHeight() .fillMaxHeight()
.windowInsetsPadding(WindowInsets.safeDrawing) .windowInsetsPadding(WindowInsets.safeDrawing)
.navigationBarsPadding() .navigationBarsPadding()
@@ -199,12 +201,20 @@ private fun TrackSelectionPanel(
.heightIn(max = 292.dp) .heightIn(max = 292.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
options.forEach { option -> options.forEachIndexed { index, option ->
val selected = option.id == selectedId val selected = option.id == selectedId
TrackOptionItem( TrackOptionItem(
label = option.label, label = option.label,
selected = selected, selected = selected,
onClick = { onSelect(option) } onClick = { onSelect(option) },
modifier = Modifier.testTag(
when {
selected -> PlayerTrackSelectedItemTag
index == 0 -> PlayerTrackFirstItemTag
index == options.lastIndex -> PlayerTrackLastItemTag
else -> "$PlayerTrackItemTagPrefix$index"
}
)
) )
} }
} }
@@ -213,6 +223,15 @@ private fun TrackSelectionPanel(
} }
} }
internal const val PlayerQualityButtonTag = "player_quality_button"
internal const val PlayerAudioButtonTag = "player_audio_button"
internal const val PlayerSubtitlesButtonTag = "player_subtitles_button"
internal const val PlayerTrackPanelTag = "player_track_panel"
internal const val PlayerTrackSelectedItemTag = "player_track_selected_item"
internal const val PlayerTrackFirstItemTag = "player_track_first_item"
internal const val PlayerTrackLastItemTag = "player_track_last_item"
internal const val PlayerTrackItemTagPrefix = "player_track_item_"
@Composable @Composable
private fun TrackOptionItem( private fun TrackOptionItem(
label: String, label: String,

View File

@@ -50,6 +50,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -146,14 +147,17 @@ internal fun SeriesActionButtons(
text = mediaPlayButtonText(nextUpEpisode?.progress, nextUpEpisode?.watched), text = mediaPlayButtonText(nextUpEpisode?.progress, nextUpEpisode?.watched),
progress = mediaPlaybackProgress(nextUpEpisode?.progress), progress = mediaPlaybackProgress(nextUpEpisode?.progress),
onClick = playAction ?: {}, onClick = playAction ?: {},
modifier = Modifier.sizeIn(maxWidth = 200.dp) modifier = Modifier
.sizeIn(maxWidth = 200.dp)
.testTag(SeriesPlayButtonTag)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
MediaActionButton( MediaActionButton(
backgroundColor = MaterialTheme.colorScheme.surface, backgroundColor = MaterialTheme.colorScheme.surface,
iconColor = MaterialTheme.colorScheme.onSurface, iconColor = MaterialTheme.colorScheme.onSurface,
icon = Icons.Outlined.Add, icon = Icons.Outlined.Add,
height = 48.dp height = 48.dp,
modifier = Modifier.testTag(SeriesAddButtonTag)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
MediaActionButton( MediaActionButton(
@@ -165,6 +169,7 @@ internal fun SeriesActionButtons(
else -> Icons.Outlined.Download else -> Icons.Outlined.Download
}, },
height = 48.dp, height = 48.dp,
modifier = Modifier.testTag(SeriesDownloadButtonTag),
onClick = { showDownloadDialog = true } onClick = { showDownloadDialog = true }
) )
} }
@@ -200,6 +205,7 @@ private fun DownloadOptionsDialog(
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
modifier = Modifier.testTag(SeriesDownloadDialogTag),
title = { Text("Download") }, title = { Text("Download") },
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -211,7 +217,10 @@ private fun DownloadOptionsDialog(
}, },
confirmButton = { confirmButton = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { onDownloadOptionSelected(SeriesDownloadOption.SEASON) }) { TextButton(
onClick = { onDownloadOptionSelected(SeriesDownloadOption.SEASON) },
modifier = Modifier.testTag(SeriesDownloadSeasonButtonTag)
) {
Text( Text(
when (seasonDownloadState) { when (seasonDownloadState) {
is DownloadState.Downloaded -> "$selectedSeasonName Downloaded" is DownloadState.Downloaded -> "$selectedSeasonName Downloaded"
@@ -220,7 +229,10 @@ private fun DownloadOptionsDialog(
} }
) )
} }
TextButton(onClick = { onDownloadOptionSelected(SeriesDownloadOption.SERIES) }) { TextButton(
onClick = { onDownloadOptionSelected(SeriesDownloadOption.SERIES) },
modifier = Modifier.testTag(SeriesDownloadAllButtonTag)
) {
Text("Download All") Text("Download All")
} }
} }
@@ -229,6 +241,7 @@ private fun DownloadOptionsDialog(
if (isSmartDownloadEnabled) { if (isSmartDownloadEnabled) {
TextButton( TextButton(
onClick = { onDownloadOptionSelected(SeriesDownloadOption.DELETE_SMART) }, onClick = { onDownloadOptionSelected(SeriesDownloadOption.DELETE_SMART) },
modifier = Modifier.testTag(SeriesSmartDownloadButtonTag),
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error contentColor = MaterialTheme.colorScheme.error
) )
@@ -236,7 +249,10 @@ private fun DownloadOptionsDialog(
Text("Delete Smart Downloads") Text("Delete Smart Downloads")
} }
} else { } else {
TextButton(onClick = { onDownloadOptionSelected(SeriesDownloadOption.SMART) }) { TextButton(
onClick = { onDownloadOptionSelected(SeriesDownloadOption.SMART) },
modifier = Modifier.testTag(SeriesSmartDownloadButtonTag)
) {
Text("Smart Download") Text("Smart Download")
} }
} }
@@ -258,11 +274,13 @@ internal fun SeasonTabs(
horizontalArrangement = Arrangement.spacedBy(20.dp), horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
seasons.forEach { season -> seasons.forEachIndexed { index, season ->
SeasonTab( SeasonTab(
name = season.name, name = season.name,
isSelected = season == selectedSeason, isSelected = season == selectedSeason,
modifier = Modifier.clickable { onSelect(season) } modifier = Modifier
.testTag("$SeriesSeasonTabTagPrefix$index")
.clickable { onSelect(season) }
) )
} }
} }
@@ -313,7 +331,7 @@ internal fun EpisodeCarousel(episodes: List<Episode>, modifier: Modifier = Modif
LazyRow( LazyRow(
state = listState, state = listState,
modifier = modifier, modifier = modifier.testTag(SeriesEpisodeCarouselTag),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
items(episodes, key = { episode -> episode.id }) { episode -> items(episodes, key = { episode -> episode.id }) { episode ->
@@ -332,6 +350,7 @@ private fun EpisodeCard(
Column( Column(
modifier = Modifier modifier = Modifier
.width(260.dp) .width(260.dp)
.testTag("$SeriesEpisodeCardTagPrefix${episode.id}")
.clickable { viewModel.onSelectEpisode( .clickable { viewModel.onSelectEpisode(
seriesId = episode.seriesId, seriesId = episode.seriesId,
seasonId = episode.seasonId, seasonId = episode.seasonId,
@@ -416,6 +435,17 @@ private fun EpisodeCard(
} }
} }
internal const val SeriesPlayButtonTag = "series-play-button"
internal const val SeriesAddButtonTag = "series-add-button"
internal const val SeriesDownloadButtonTag = "series-download-button"
internal const val SeriesDownloadDialogTag = "series-download-dialog"
internal const val SeriesDownloadSeasonButtonTag = "series-download-season-button"
internal const val SeriesDownloadAllButtonTag = "series-download-all-button"
internal const val SeriesSmartDownloadButtonTag = "series-smart-download-button"
internal const val SeriesSeasonTabTagPrefix = "series-season-tab-"
internal const val SeriesEpisodeCarouselTag = "series-episode-carousel"
internal const val SeriesEpisodeCardTagPrefix = "series-episode-card-"
@Composable @Composable
internal fun CastRow(cast: List<CastMember>, modifier: Modifier = Modifier) { internal fun CastRow(cast: List<CastMember>, modifier: Modifier = Modifier) {
//TODO fix //TODO fix