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:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user