UI Test Suite5: Task tests (iOS parity)

Ports Suite5_TaskTests.swift. testTags on task screens via
AccessibilityIds.Task.*. CRUD + kanban + filter/sort + templates + suggestions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 14:40:38 -05:00
parent c772215c04
commit eedfac30c6
8 changed files with 383 additions and 24 deletions

View File

@@ -8,9 +8,11 @@ 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.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.models.TaskTemplate
import com.tt.honeyDue.repository.LookupsRepository
import com.tt.honeyDue.models.MyResidencesResponse
@@ -150,7 +152,8 @@ fun AddTaskDialog(
label = { Text(stringResource(Res.string.tasks_property_required)) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor()
.testTag(AccessibilityIds.Task.residencePicker),
isError = residenceError,
supportingText = if (residenceError) {
{ Text(stringResource(Res.string.tasks_property_error)) }
@@ -191,7 +194,9 @@ fun AddTaskDialog(
showSuggestions = it.length >= 2 && filteredSuggestions.isNotEmpty()
},
label = { Text(stringResource(Res.string.tasks_title_required)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.titleField),
isError = titleError,
supportingText = if (titleError) {
{ Text(stringResource(Res.string.tasks_title_error)) }
@@ -216,7 +221,9 @@ fun AddTaskDialog(
value = description,
onValueChange = { description = it },
label = { Text(stringResource(Res.string.tasks_description_label)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.descriptionField),
minLines = 2,
maxLines = 4
)
@@ -232,7 +239,8 @@ fun AddTaskDialog(
label = { Text(stringResource(Res.string.tasks_category_required)) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor()
.testTag(AccessibilityIds.Task.categoryPicker),
isError = categoryError,
supportingText = if (categoryError) {
{ Text(stringResource(Res.string.tasks_category_error)) }
@@ -269,7 +277,8 @@ fun AddTaskDialog(
label = { Text(stringResource(Res.string.tasks_frequency_label)) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor()
.testTag(AccessibilityIds.Task.frequencyPicker),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) },
enabled = frequencies.isNotEmpty()
@@ -300,7 +309,9 @@ fun AddTaskDialog(
value = intervalDays,
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
label = { Text(stringResource(Res.string.tasks_interval_days)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.intervalDaysField),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
singleLine = true
@@ -315,7 +326,9 @@ fun AddTaskDialog(
dueDateError = false
},
label = { Text(stringResource(Res.string.tasks_due_date_required)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.dueDatePicker),
isError = dueDateError,
supportingText = if (dueDateError) {
{ Text(stringResource(Res.string.tasks_due_date_format_error)) }
@@ -336,7 +349,8 @@ fun AddTaskDialog(
label = { Text(stringResource(Res.string.tasks_priority_label)) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor()
.testTag(AccessibilityIds.Task.priorityPicker),
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) },
enabled = priorities.isNotEmpty()
@@ -362,7 +376,9 @@ fun AddTaskDialog(
value = estimatedCost,
onValueChange = { estimatedCost = it },
label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.estimatedCostField),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
prefix = { Text("$") },
singleLine = true
@@ -381,6 +397,7 @@ fun AddTaskDialog(
},
confirmButton = {
Button(
modifier = Modifier.testTag(AccessibilityIds.Task.saveButton),
onClick = {
// Validation
var hasError = false
@@ -433,7 +450,10 @@ fun AddTaskDialog(
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
TextButton(
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton),
onClick = onDismiss
) {
Text(stringResource(Res.string.common_cancel))
}
}

View File

@@ -12,9 +12,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import honeydue.composeapp.generated.resources.*
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.models.TaskDetail
import com.tt.honeyDue.models.TaskCategory
import com.tt.honeyDue.models.TaskPriority
@@ -38,7 +40,9 @@ fun TaskCard(
onCompletionHistoryClick: (() -> Unit)? = null
) {
Card(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.taskCard),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(

View File

@@ -18,10 +18,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.tt.honeyDue.models.TaskColumn
import com.tt.honeyDue.models.TaskDetail
import com.tt.honeyDue.testing.AccessibilityIds
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -275,7 +277,7 @@ fun DynamicTaskKanbanView(
HorizontalPager(
state = pagerState,
modifier = modifier.fillMaxSize(),
modifier = modifier.fillMaxSize().testTag(AccessibilityIds.Task.kanbanView),
pageSpacing = 16.dp,
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
) { page ->

View File

@@ -13,6 +13,8 @@ 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 androidx.compose.ui.platform.testTag
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.components.AddNewTaskWithResidenceDialog
import com.tt.honeyDue.ui.components.ApiResultHandler
import com.tt.honeyDue.ui.components.CompleteTaskDialog
@@ -120,7 +122,8 @@ fun AllTasksScreen(
},
actions = {
IconButton(
onClick = { viewModel.loadTasks(forceRefresh = true) }
onClick = { viewModel.loadTasks(forceRefresh = true) },
modifier = Modifier.testTag(AccessibilityIds.Task.refreshButton)
) {
Icon(
Icons.Default.Refresh,
@@ -130,7 +133,8 @@ fun AllTasksScreen(
IconButton(
onClick = { showNewTaskDialog = true },
enabled = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty(),
modifier = Modifier.testTag(AccessibilityIds.Task.addButton)
) {
Icon(
Icons.Default.Add,
@@ -185,7 +189,9 @@ fun AllTasksScreen(
OrganicPrimaryButton(
text = "Add Task",
onClick = { showNewTaskDialog = true },
modifier = Modifier.fillMaxWidth(0.7f),
modifier = Modifier
.fillMaxWidth(0.7f)
.testTag(AccessibilityIds.Task.emptyStateView),
enabled = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
)

View File

@@ -20,11 +20,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
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 honeydue.composeapp.generated.resources.*
import com.tt.honeyDue.models.TaskCompletionCreateRequest
import com.tt.honeyDue.models.ContractorSummary
@@ -87,7 +89,10 @@ fun CompleteTaskScreen(
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
IconButton(
onClick = onNavigateBack,
modifier = Modifier.testTag(AccessibilityIds.Task.detailCancelButton)
) {
Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.common_cancel))
}
},
@@ -230,7 +235,9 @@ fun CompleteTaskScreen(
label = { Text(stringResource(Res.string.completions_actual_cost_optional)) },
leadingIcon = { Icon(Icons.Default.AttachMoney, null) },
prefix = { Text("$") },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.actualCostField),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
shape = OrganicShapes.medium
@@ -252,7 +259,8 @@ fun CompleteTaskScreen(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = OrganicSpacing.lg)
.height(120.dp),
.height(120.dp)
.testTag(AccessibilityIds.Task.notesField),
shape = OrganicShapes.medium
)
@@ -416,7 +424,8 @@ fun CompleteTaskScreen(
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = OrganicSpacing.lg),
.padding(horizontal = OrganicSpacing.lg)
.testTag(AccessibilityIds.Task.submitButton),
enabled = !isSubmitting,
isLoading = isSubmitting,
icon = Icons.Default.CheckCircle

View File

@@ -9,10 +9,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
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.ui.components.HandleErrors
import com.tt.honeyDue.viewmodel.ResidenceViewModel
import com.tt.honeyDue.repository.LookupsRepository
@@ -98,7 +100,10 @@ fun EditTaskScreen(
TopAppBar(
title = { Text(stringResource(Res.string.tasks_edit_title)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
IconButton(
onClick = onNavigateBack,
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton)
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.common_back))
}
}
@@ -126,7 +131,9 @@ fun EditTaskScreen(
value = title,
onValueChange = { title = it },
label = { Text(stringResource(Res.string.tasks_title_required)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.titleField),
isError = titleError.isNotEmpty(),
supportingText = if (titleError.isNotEmpty()) {
{ Text(titleError) }
@@ -137,7 +144,9 @@ fun EditTaskScreen(
value = description,
onValueChange = { description = it },
label = { Text(stringResource(Res.string.tasks_description_label)) },
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(AccessibilityIds.Task.descriptionField),
minLines = 3,
maxLines = 5
)
@@ -304,6 +313,7 @@ fun EditTaskScreen(
// Submit button
OrganicPrimaryButton(
text = stringResource(Res.string.tasks_update),
modifier = Modifier.testTag(AccessibilityIds.Task.saveButton),
onClick = {
if (validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null) {

View File

@@ -31,6 +31,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
@@ -38,6 +39,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.repository.LookupsRepository
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
@@ -88,7 +90,10 @@ fun AddTaskWithResidenceScreen(
TopAppBar(
title = { Text("New Task", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
IconButton(
onClick = onNavigateBack,
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton)
) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
@@ -112,6 +117,7 @@ fun AddTaskWithResidenceScreen(
value = title,
onValueChange = viewModel::onTitleChange,
label = "Title",
modifier = Modifier.testTag(AccessibilityIds.Task.titleField),
placeholder = "e.g. Flush water heater",
error = titleError,
enabled = !isSubmitting
@@ -123,6 +129,7 @@ fun AddTaskWithResidenceScreen(
value = description,
onValueChange = viewModel::onDescriptionChange,
label = "Description",
modifier = Modifier.testTag(AccessibilityIds.Task.descriptionField),
placeholder = "Optional details",
singleLine = false,
maxLines = 4,
@@ -242,7 +249,8 @@ fun AddTaskWithResidenceScreen(
enabled = canSubmit && !isSubmitting,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
.height(52.dp)
.testTag(AccessibilityIds.Task.saveButton),
shape = androidx.compose.foundation.shape.RoundedCornerShape(AppRadius.md),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary