feat(#76): add testTag identifiers to composable elements for UI test targeting (#98)

Adds testTag modifiers to key interactive elements across the core
composables, enabling Espresso/Compose UI tests to locate them:

Navigation.kt  — 'nav_*' on bottom bar items, 'drawer_*' on drawer items
HermesScaffold — 'back_button', 'menu_button', 'refresh_button'
StateViews.kt  — 'loading_state', 'loading_spinner', 'error_retry_button',
                 'empty_state_action'
ChatScreen.kt  — 'chat_input', 'send_button', 'interrupt_button'

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-19 22:15:53 +03:00
1 parent cc52442be3
commit abcec14cb5
4 files changed
+38 -8

No files matched your search

@@ -56,6 +56,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.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
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.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
@@ -279,7 +280,10 @@ fun MainNavigation() {
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer, selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
), ),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp), modifier =
Modifier
.padding(horizontal = 12.dp, vertical = 2.dp)
.testTag("drawer_${entry.label.lowercase().replace(' ', '_')}"),
) )
} }
} }
@@ -302,6 +306,7 @@ fun MainNavigation() {
onClick = { NavigationController.navigateTo(item.key) }, onClick = { NavigationController.navigateTo(item.key) },
icon = { Icon(item.icon, contentDescription = item.label) }, icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) }, label = { Text(item.label) },
modifier = Modifier.testTag("nav_${item.label.lowercase().replace(' ', '_')}"),
) )
} }
} }
@@ -84,6 +84,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
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.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -650,7 +651,8 @@ private fun ChatInputBar(
modifier = modifier =
Modifier Modifier
.weight(1f) .weight(1f)
.heightIn(max = 140.dp), .heightIn(max = 140.dp)
.testTag("chat_input"),
placeholder = { placeholder = {
Text( Text(
if (!isConnected) { if (!isConnected) {
@@ -679,7 +681,10 @@ private fun ChatInputBar(
// Interrupt button // Interrupt button
FilledTonalButton( FilledTonalButton(
onClick = onInterrupt, onClick = onInterrupt,
modifier = Modifier.size(48.dp), modifier =
Modifier
.size(48.dp)
.testTag("interrupt_button"),
shape = CircleShape, shape = CircleShape,
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
) { ) {
@@ -694,7 +699,10 @@ private fun ChatInputBar(
FilledTonalButton( FilledTonalButton(
onClick = onSend, onClick = onSend,
enabled = canSend, enabled = canSend,
modifier = Modifier.size(48.dp), modifier =
Modifier
.size(48.dp)
.testTag("send_button"),
shape = CircleShape, shape = CircleShape,
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
) { ) {
@@ -20,6 +20,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
/** /**
* Shared Scaffold wrapper that standardizes the TopAppBar. * Shared Scaffold wrapper that standardizes the TopAppBar.
@@ -69,6 +70,7 @@ fun HermesScaffold(
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
modifier = Modifier.testTag("back_button"),
) )
} }
} }
@@ -78,6 +80,7 @@ fun HermesScaffold(
Icon( Icon(
imageVector = Icons.Filled.Menu, imageVector = Icons.Filled.Menu,
contentDescription = "Open navigation drawer", contentDescription = "Open navigation drawer",
modifier = Modifier.testTag("menu_button"),
) )
} }
} }
@@ -89,6 +92,7 @@ fun HermesScaffold(
Icon( Icon(
imageVector = Icons.Filled.Refresh, imageVector = Icons.Filled.Refresh,
contentDescription = "Refresh", contentDescription = "Refresh",
modifier = Modifier.testTag("refresh_button"),
) )
} }
} }
@@ -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.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
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
import com.m57.hermescontrol.theme.LocalSpacing import com.m57.hermescontrol.theme.LocalSpacing
@@ -34,13 +35,19 @@ import com.m57.hermescontrol.theme.LocalSpacing
@Composable @Composable
fun LoadingState(modifier: Modifier = Modifier) { fun LoadingState(modifier: Modifier = Modifier) {
Box( Box(
modifier = modifier.fillMaxSize(), modifier =
modifier
.fillMaxSize()
.testTag("loading_state"),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
strokeWidth = 3.dp, strokeWidth = 3.dp,
modifier = Modifier.size(40.dp), modifier =
Modifier
.size(40.dp)
.testTag("loading_spinner"),
) )
} }
} }
@@ -77,7 +84,10 @@ fun ErrorState(
) )
if (onRetry != null) { if (onRetry != null) {
Spacer(modifier = Modifier.height(spacing.md)) Spacer(modifier = Modifier.height(spacing.md))
Button(onClick = onRetry) { Button(
onClick = onRetry,
modifier = Modifier.testTag("error_retry_button"),
) {
Icon( Icon(
imageVector = Icons.Filled.Refresh, imageVector = Icons.Filled.Refresh,
contentDescription = null, contentDescription = null,
@@ -133,7 +143,10 @@ fun EmptyState(
} }
if (actionLabel != null && onAction != null) { if (actionLabel != null && onAction != null) {
Spacer(modifier = Modifier.height(spacing.md)) Spacer(modifier = Modifier.height(spacing.md))
OutlinedButton(onClick = onAction) { OutlinedButton(
onClick = onAction,
modifier = Modifier.testTag("empty_state_action"),
) {
Text(actionLabel) Text(actionLabel)
} }
} }