UI Test Suite1: Registration + SimpleLogin ports (iOS parity)

Ports iOS Suite1_RegistrationTests.swift + SimpleLoginTest.swift to
Android Compose UI Test. Adds testTag annotations on auth screens using
shared AccessibilityIds.Authentication constants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 14:38:56 -05:00
parent b97db89737
commit 95dabf741f
6 changed files with 535 additions and 16 deletions

View File

@@ -29,11 +29,19 @@ object AccessibilityIds {
const val registerConfirmPasswordField = "Register.ConfirmPasswordField"
const val registerButton = "Register.RegisterButton"
const val registerCancelButton = "Register.CancelButton"
// Error text rendered on the registration screen (e.g., weak password,
// mismatched password, API validation errors). Used by Suite1 tests to
// verify negative registration cases stay on the form.
const val registerErrorMessage = "Register.ErrorMessage"
// Verification
const val verificationCodeField = "Verification.CodeField"
const val verifyButton = "Verification.VerifyButton"
const val resendCodeButton = "Verification.ResendButton"
// Logout affordance surfaced in the verify-email toolbar. iOS exposes
// this via `AccessibilityIdentifiers.Authentication.verificationLogoutButton`
// in its production helper; parity tests rely on this tag.
const val verificationLogoutButton = "Verification.LogoutButton"
}
// MARK: - Navigation
@@ -83,6 +91,21 @@ object AccessibilityIds {
const val manageUsersButton = "ResidenceDetail.ManageUsersButton"
const val tasksSection = "ResidenceDetail.TasksSection"
const val addTaskButton = "ResidenceDetail.AddTaskButton"
// List auxiliary (Android-only additions, kept as supersets)
const val joinButton = "Residence.JoinButton"
const val addFab = "Residence.AddFab"
// Detail auxiliary (Android-only additions)
const val confirmDeleteButton = "ResidenceDetail.ConfirmDeleteButton"
// Join (full-screen Join Residence flow — matches iOS feature tests)
const val joinShareCodeField = "JoinResidence.ShareCodeField"
const val joinSubmitButton = "JoinResidence.JoinButton"
// Manage Users (full-screen Manage Users flow — matches iOS feature tests)
const val manageUsersList = "ManageUsers.UsersList"
const val manageUsersRemoveButton = "ManageUsers.RemoveButton"
}
// MARK: - Task
@@ -155,6 +178,9 @@ object AccessibilityIds {
const val deleteButton = "ContractorDetail.DeleteButton"
const val callButton = "ContractorDetail.CallButton"
const val emailButton = "ContractorDetail.EmailButton"
// Android-only: share button exposed directly in detail top bar; iOS
// surfaces share via the system share sheet from the ellipsis menu.
const val shareButton = "ContractorDetail.ShareButton"
}
// MARK: - Document

View File

@@ -14,6 +14,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -22,6 +25,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.components.HandleErrors
import com.tt.honeyDue.ui.components.auth.AuthHeader
import com.tt.honeyDue.ui.components.auth.GoogleSignInButton
@@ -96,10 +100,12 @@ fun LoginScreen(
val isLoading = loginState is ApiResult.Loading || googleSignInState is ApiResult.Loading
WarmGradientBackground {
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
Box(
modifier = Modifier
.fillMaxSize()
.imePadding(),
.imePadding()
.semantics { testTagsAsResourceId = true },
contentAlignment = Alignment.Center
) {
OrganicCard(
@@ -131,7 +137,9 @@ fun LoginScreen(
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null) // decorative
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.usernameField),
singleLine = true,
shape = RoundedCornerShape(OrganicRadius.md),
keyboardOptions = KeyboardOptions(
@@ -148,14 +156,19 @@ fun LoginScreen(
Icon(Icons.Default.Lock, contentDescription = null) // decorative
},
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
IconButton(
onClick = { passwordVisible = !passwordVisible },
modifier = Modifier.testTag(AccessibilityIds.Authentication.passwordVisibilityToggle)
) {
Icon(
imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = if (passwordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password)
)
}
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.passwordField),
singleLine = true,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(OrganicRadius.md)
@@ -174,7 +187,9 @@ fun LoginScreen(
onClick = {
viewModel.login(username, password)
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.loginButton),
enabled = username.isNotEmpty() && password.isNotEmpty(),
isLoading = isLoading
)
@@ -214,7 +229,9 @@ fun LoginScreen(
TextButton(
onClick = onNavigateToForgotPassword,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.forgotPasswordButton)
) {
Text(
stringResource(Res.string.auth_forgot_password),
@@ -225,7 +242,9 @@ fun LoginScreen(
TextButton(
onClick = onNavigateToRegister,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.signUpButton)
) {
Text(
stringResource(Res.string.auth_no_account),

View File

@@ -11,11 +11,15 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.components.HandleErrors
import com.tt.honeyDue.ui.components.auth.AuthHeader
import com.tt.honeyDue.ui.components.auth.RequirementItem
@@ -70,12 +74,17 @@ fun RegisterScreen(
}
WarmGradientBackground {
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
Scaffold(
modifier = Modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = { Text(stringResource(Res.string.auth_register_title), fontWeight = FontWeight.SemiBold) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
IconButton(
onClick = onNavigateBack,
modifier = Modifier.testTag(AccessibilityIds.Authentication.registerCancelButton)
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.common_back))
}
},
@@ -119,7 +128,9 @@ fun RegisterScreen(
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null) // decorative
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.registerUsernameField),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
@@ -131,7 +142,9 @@ fun RegisterScreen(
leadingIcon = {
Icon(Icons.Default.Email, contentDescription = null) // decorative
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.registerEmailField),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
@@ -145,7 +158,9 @@ fun RegisterScreen(
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null) // decorative
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.registerPasswordField),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
@@ -158,7 +173,9 @@ fun RegisterScreen(
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null) // decorative
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.registerConfirmPasswordField),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
@@ -196,7 +213,10 @@ fun RegisterScreen(
}
}
ErrorCard(message = errorMessage)
ErrorCard(
message = errorMessage,
modifier = Modifier.testTag(AccessibilityIds.Authentication.registerErrorMessage)
)
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
@@ -227,7 +247,9 @@ fun RegisterScreen(
}
}
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.registerButton),
enabled = username.isNotEmpty() && email.isNotEmpty() &&
password.isNotEmpty() && isPasswordComplex && passwordsMatch && !isLoading,
isLoading = isLoading

View File

@@ -10,12 +10,16 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.components.HandleErrors
import com.tt.honeyDue.ui.components.auth.AuthHeader
import com.tt.honeyDue.ui.components.common.ErrorCard
@@ -65,12 +69,17 @@ fun VerifyEmailScreen(
}
}
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
Scaffold(
modifier = Modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = { Text(stringResource(Res.string.auth_verify_title), fontWeight = FontWeight.SemiBold) },
actions = {
TextButton(onClick = onLogout) {
TextButton(
onClick = onLogout,
modifier = Modifier.testTag(AccessibilityIds.Authentication.verificationLogoutButton)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
@@ -164,7 +173,9 @@ fun VerifyEmailScreen(
leadingIcon = {
Icon(Icons.Default.Pin, contentDescription = null) // decorative
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Authentication.verificationCodeField),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
placeholder = { Text("000000") }
@@ -190,6 +201,7 @@ fun VerifyEmailScreen(
errorMessage = "Please enter a valid 6-digit code"
}
},
modifier = Modifier.testTag(AccessibilityIds.Authentication.verifyButton),
enabled = !isLoading && code.length == 6,
isLoading = isLoading
)