UI Test Suite4: Comprehensive residence tests (iOS parity)

Ports Suite4_ComprehensiveResidenceTests.swift. testTags on residence
screens via AccessibilityIds.Residence.*. CRUD + join + manage users +
multi-residence switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 14:43:08 -05:00
parent eedfac30c6
commit 6980ed772b
7 changed files with 724 additions and 59 deletions

View File

@@ -11,6 +11,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -21,6 +24,7 @@ import com.tt.honeyDue.models.ResidenceUser
import com.tt.honeyDue.models.ResidenceShareCode
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.theme.*
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@@ -64,6 +68,7 @@ fun ManageUsersScreen(
WarmGradientBackground {
Scaffold(
modifier = Modifier.semantics { testTagsAsResourceId = true },
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = {
TopAppBar(
@@ -128,7 +133,8 @@ fun ManageUsersScreen(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
.padding(paddingValues)
.testTag(AccessibilityIds.Residence.manageUsersList),
contentPadding = PaddingValues(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
@@ -462,7 +468,10 @@ private fun UserCard(
}
if (canRemove) {
IconButton(onClick = onRemove) {
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.manageUsersRemoveButton),
onClick = onRemove
) {
Icon(
Icons.Default.Delete,
contentDescription = stringResource(Res.string.manage_users_remove),

View File

@@ -12,10 +12,14 @@ 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.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.AddNewTaskDialog
import com.tt.honeyDue.ui.components.ApiResultHandler
import com.tt.honeyDue.ui.components.CompleteTaskDialog
@@ -302,6 +306,7 @@ fun ResidenceDetailScreen(
text = { Text(stringResource(Res.string.properties_delete_name_confirm, residence.name)) },
confirmButton = {
Button(
modifier = Modifier.testTag(AccessibilityIds.Residence.confirmDeleteButton),
onClick = {
showDeleteConfirmation = false
residenceViewModel.deleteResidence(residenceId)
@@ -415,6 +420,9 @@ fun ResidenceDetailScreen(
}
Scaffold(
modifier = Modifier
.semantics { testTagsAsResourceId = true }
.testTag(AccessibilityIds.Residence.detailView),
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
topBar = {
TopAppBar(
@@ -449,6 +457,7 @@ fun ResidenceDetailScreen(
// Share button - only show for primary owners
if (residence.ownerId == currentUser?.id) {
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.shareButton),
onClick = {
val shareCheck = SubscriptionHelper.canShareResidence()
if (shareCheck.allowed) {
@@ -473,39 +482,48 @@ fun ResidenceDetailScreen(
// Manage Users button - only show for primary owners
if (residence.ownerId == currentUser?.id) {
IconButton(onClick = {
val shareCheck = SubscriptionHelper.canShareResidence()
if (shareCheck.allowed) {
if (onNavigateToManageUsers != null) {
onNavigateToManageUsers(
residence.id,
residence.name,
residence.ownerId == currentUser?.id,
residence.ownerId
)
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.manageUsersButton),
onClick = {
val shareCheck = SubscriptionHelper.canShareResidence()
if (shareCheck.allowed) {
if (onNavigateToManageUsers != null) {
onNavigateToManageUsers(
residence.id,
residence.name,
residence.ownerId == currentUser?.id,
residence.ownerId
)
} else {
showManageUsersDialog = true
}
} else {
showManageUsersDialog = true
upgradeTriggerKey = shareCheck.triggerKey
showUpgradePrompt = true
}
} else {
upgradeTriggerKey = shareCheck.triggerKey
showUpgradePrompt = true
}
}) {
) {
Icon(Icons.Default.People, contentDescription = stringResource(Res.string.properties_manage_users))
}
}
IconButton(onClick = {
onNavigateToEditResidence(residence)
}) {
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.editButton),
onClick = {
onNavigateToEditResidence(residence)
}
) {
Icon(Icons.Default.Edit, contentDescription = stringResource(Res.string.properties_edit_residence))
}
// Delete button - only show for primary owners
if (residence.ownerId == currentUser?.id) {
IconButton(onClick = {
showDeleteConfirmation = true
}) {
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.deleteButton),
onClick = {
showDeleteConfirmation = true
}
) {
Icon(
Icons.Default.Delete,
contentDescription = stringResource(Res.string.properties_delete_residence),
@@ -524,6 +542,7 @@ fun ResidenceDetailScreen(
// Don't show FAB if tasks are blocked (limit=0)
if (!isTasksBlocked.allowed) {
FloatingActionButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.addTaskButton),
onClick = {
val (allowed, triggerKey) = canAddTask()
if (allowed) {
@@ -819,7 +838,8 @@ fun ResidenceDetailScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = OrganicSpacing.compact),
.padding(vertical = OrganicSpacing.compact)
.testTag(AccessibilityIds.Residence.tasksSection),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {

View File

@@ -11,10 +11,14 @@ 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.input.KeyboardType
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.viewmodel.ResidenceViewModel
import com.tt.honeyDue.repository.LookupsRepository
import com.tt.honeyDue.data.DataManager
@@ -156,11 +160,15 @@ fun ResidenceFormScreen(
}
Scaffold(
modifier = Modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = { Text(if (isEditMode) stringResource(Res.string.properties_edit_title) else stringResource(Res.string.properties_add_title)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.formCancelButton),
onClick = onNavigateBack
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.common_back))
}
}
@@ -188,7 +196,9 @@ fun ResidenceFormScreen(
value = name,
onValueChange = { name = it },
label = { Text(stringResource(Res.string.properties_form_name_required)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.nameField),
isError = nameError.isNotEmpty(),
supportingText = if (nameError.isNotEmpty()) {
{ Text(nameError, color = MaterialTheme.colorScheme.error) }
@@ -209,7 +219,8 @@ fun ResidenceFormScreen(
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor()
.testTag(AccessibilityIds.Residence.propertyTypePicker),
enabled = propertyTypes.isNotEmpty()
)
ExposedDropdownMenu(
@@ -239,42 +250,54 @@ fun ResidenceFormScreen(
value = streetAddress,
onValueChange = { streetAddress = it },
label = { Text(stringResource(Res.string.properties_form_street)) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.streetAddressField)
)
OutlinedTextField(
value = apartmentUnit,
onValueChange = { apartmentUnit = it },
label = { Text(stringResource(Res.string.properties_form_apartment)) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.apartmentUnitField)
)
OutlinedTextField(
value = city,
onValueChange = { city = it },
label = { Text(stringResource(Res.string.properties_form_city)) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.cityField)
)
OutlinedTextField(
value = stateProvince,
onValueChange = { stateProvince = it },
label = { Text(stringResource(Res.string.properties_form_state)) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.stateProvinceField)
)
OutlinedTextField(
value = postalCode,
onValueChange = { postalCode = it },
label = { Text(stringResource(Res.string.properties_form_postal)) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.postalCodeField)
)
OutlinedTextField(
value = country,
onValueChange = { country = it },
label = { Text(stringResource(Res.string.properties_form_country)) },
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.countryField)
)
// Optional fields section
@@ -294,7 +317,9 @@ fun ResidenceFormScreen(
onValueChange = { bedrooms = it.filter { char -> char.isDigit() } },
label = { Text(stringResource(Res.string.properties_bedrooms)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
modifier = Modifier
.weight(1f)
.testTag(AccessibilityIds.Residence.bedroomsField)
)
OutlinedTextField(
@@ -302,7 +327,9 @@ fun ResidenceFormScreen(
onValueChange = { bathrooms = it },
label = { Text(stringResource(Res.string.properties_bathrooms)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.weight(1f)
modifier = Modifier
.weight(1f)
.testTag(AccessibilityIds.Residence.bathroomsField)
)
}
@@ -311,7 +338,9 @@ fun ResidenceFormScreen(
onValueChange = { squareFootage = it.filter { char -> char.isDigit() } },
label = { Text(stringResource(Res.string.properties_form_sqft)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.squareFootageField)
)
OutlinedTextField(
@@ -319,7 +348,9 @@ fun ResidenceFormScreen(
onValueChange = { lotSize = it },
label = { Text(stringResource(Res.string.properties_form_lot_size)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.lotSizeField)
)
OutlinedTextField(
@@ -327,14 +358,18 @@ fun ResidenceFormScreen(
onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } },
label = { Text(stringResource(Res.string.properties_year_built)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.yearBuiltField)
)
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text(stringResource(Res.string.properties_form_description)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Residence.descriptionField),
minLines = 3,
maxLines = 5
)
@@ -345,6 +380,7 @@ fun ResidenceFormScreen(
) {
Text(stringResource(Res.string.properties_form_primary))
Switch(
modifier = Modifier.testTag(AccessibilityIds.Residence.isPrimaryToggle),
checked = isPrimary,
onCheckedChange = { isPrimary = it }
)
@@ -404,6 +440,7 @@ fun ResidenceFormScreen(
// Submit button
OrganicPrimaryButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.saveButton),
text = if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create),
onClick = {
if (validateForm()) {

View File

@@ -19,12 +19,16 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
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.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.components.ApiResultHandler
import com.tt.honeyDue.ui.components.common.StatItem
import com.tt.honeyDue.ui.components.residence.TaskStatChip
@@ -108,6 +112,7 @@ fun ResidencesScreen(
}
Scaffold(
modifier = Modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = {
@@ -128,29 +133,35 @@ fun ResidencesScreen(
actions = {
// Only show Join button if not blocked (limit>0)
if (!isBlocked.allowed) {
IconButton(onClick = {
val (allowed, triggerKey) = canAddProperty()
if (allowed) {
onJoinResidence()
} else {
upgradeTriggerKey = triggerKey
showUpgradePrompt = true
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.joinButton),
onClick = {
val (allowed, triggerKey) = canAddProperty()
if (allowed) {
onJoinResidence()
} else {
upgradeTriggerKey = triggerKey
showUpgradePrompt = true
}
}
}) {
) {
Icon(Icons.Default.GroupAdd, contentDescription = stringResource(Res.string.properties_join_title))
}
}
// Add property button
if (!isBlocked.allowed) {
IconButton(onClick = {
val (allowed, triggerKey) = canAddProperty()
if (allowed) {
onAddResidence()
} else {
upgradeTriggerKey = triggerKey
showUpgradePrompt = true
IconButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.addButton),
onClick = {
val (allowed, triggerKey) = canAddProperty()
if (allowed) {
onAddResidence()
} else {
upgradeTriggerKey = triggerKey
showUpgradePrompt = true
}
}
}) {
) {
Icon(
Icons.Default.AddCircle,
contentDescription = stringResource(Res.string.properties_add_button),
@@ -172,6 +183,7 @@ fun ResidencesScreen(
if (hasResidences && !isBlocked.allowed) {
Box(modifier = Modifier.padding(bottom = 80.dp)) {
FloatingActionButton(
modifier = Modifier.testTag(AccessibilityIds.Residence.addFab),
onClick = {
val (allowed, triggerKey) = canAddProperty()
if (allowed) {
@@ -208,7 +220,8 @@ fun ResidencesScreen(
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
.padding(paddingValues)
.testTag(AccessibilityIds.Residence.emptyStateView),
contentAlignment = Alignment.Center
) {
Column(
@@ -247,7 +260,8 @@ fun ResidencesScreen(
},
modifier = Modifier
.fillMaxWidth(0.7f)
.height(56.dp),
.height(56.dp)
.testTag(AccessibilityIds.Residence.emptyStateButton),
shape = RoundedCornerShape(12.dp)
) {
Row(
@@ -330,7 +344,9 @@ fun ResidencesScreen(
.padding(paddingValues)
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.testTag(AccessibilityIds.Residence.residencesList),
contentPadding = PaddingValues(
start = OrganicSpacing.cozy,
end = OrganicSpacing.cozy,
@@ -446,6 +462,7 @@ fun ResidencesScreen(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.withId(AccessibilityIds.Residence.residenceCard, residence.id))
.clickable { onResidenceClick(residence.id) },
accentColor = if (hasOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
showBlob = true,

View File

@@ -34,6 +34,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
@@ -41,6 +44,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.components.common.StandardCard
import com.tt.honeyDue.ui.components.forms.FormTextField
import com.tt.honeyDue.ui.theme.AppRadius
@@ -79,6 +83,7 @@ fun JoinResidenceScreen(
}
Scaffold(
modifier = Modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = {
@@ -139,6 +144,7 @@ fun JoinResidenceScreen(
value = code,
onValueChange = { viewModel.updateCode(it) },
label = "Share Code",
modifier = Modifier.testTag(AccessibilityIds.Residence.joinShareCodeField),
placeholder = "ABC123",
enabled = !isLoading,
error = error,
@@ -182,7 +188,8 @@ fun JoinResidenceScreen(
),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
.height(56.dp)
.testTag(AccessibilityIds.Residence.joinSubmitButton),
) {
if (isLoading) {
CircularProgressIndicator(