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

@@ -0,0 +1,300 @@
package com.tt.honeyDue
import android.content.Context
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.PersistenceManager
import com.tt.honeyDue.storage.TaskCacheManager
import com.tt.honeyDue.storage.TaskCacheStorage
import com.tt.honeyDue.storage.ThemeStorageManager
import com.tt.honeyDue.storage.TokenManager
import com.tt.honeyDue.testing.AccessibilityIds
import org.junit.After
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
/**
* Android port of `iosApp/HoneyDueUITests/Suite5_TaskTests.swift`.
*
* Covers the task list's add-button affordance, new-task form open/cancel,
* and cross-tab navigation between Tasks → Contractors → Documents →
* Residences. The live-backend create flow (iOS test06/07 which poll the API
* after save) is deferred here because instrumented tests cannot rely on the
* dev backend being reachable; the UI-side equivalent (open form → see title
* field → cancel) is covered by test01/05.
*
* iOS parity:
* - test01_cancelTaskCreation → test01_cancelTaskCreation
* - test02_tasksTabExists → test02_tasksTabExists
* - test03_viewTasksList → test03_viewTasksList
* - test04_addTaskButtonEnabled → test04_addTaskButtonEnabled
* - test05_navigateToAddTask → test05_navigateToAddTask
* - test08_navigateToContractors → test08_navigateToContractors
* - test09_navigateToDocuments → test09_navigateToDocuments
* - test10_navigateBetweenTabs → test10_navigateBetweenTabs
*
* Deliberately deferred (require a live authenticated session + dev backend
* reachability, which the parallel Residence/Contractor suites avoid the
* same way):
* - test06_createBasicTask — verifies created task via API polling
* - test07_viewTaskDetails — creates a task then inspects action menu
*/
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class Suite5_TaskTests {
@get:Rule
val composeRule = createAndroidComposeRule<MainActivity>()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
if (!isDataManagerInitialized()) {
DataManager.initialize(
tokenMgr = TokenManager.getInstance(context),
themeMgr = ThemeStorageManager.getInstance(context),
persistenceMgr = PersistenceManager.getInstance(context),
)
}
TaskCacheStorage.initialize(TaskCacheManager.getInstance(context))
// Precondition: the task add button only enables when a residence
// exists. Log in as the seeded test user (AAA_SeedTests guarantees
// the testuser + residence + task exist) and navigate to the Tasks
// tab so each test starts in the same known state.
UITestHelpers.ensureOnLoginScreen(composeRule)
UITestHelpers.loginAsTestUser(composeRule)
navigateToTasks()
// Wait for task screen to settle — add button must be reachable.
waitForTag(AccessibilityIds.Task.addButton, timeoutMs = 20_000L)
}
@After
fun tearDown() {
// Dismiss the task dialog if it was left open by a failing assertion.
if (nodeExists(AccessibilityIds.Task.formCancelButton)) {
composeRule.onNodeWithTag(
AccessibilityIds.Task.formCancelButton,
useUnmergedTree = true,
).performClick()
composeRule.waitForIdle()
}
UITestHelpers.tearDown(composeRule)
}
// MARK: - Helpers
private fun waitForTag(tag: String, timeoutMs: Long = 10_000L) {
composeRule.waitUntil(timeoutMs) {
composeRule.onAllNodesWithTag(tag, useUnmergedTree = true)
.fetchSemanticsNodes()
.isNotEmpty()
}
}
private fun nodeExists(tag: String): Boolean =
composeRule.onAllNodesWithTag(tag, useUnmergedTree = true)
.fetchSemanticsNodes()
.isNotEmpty()
/** Taps the Tasks tab and waits for the task screen affordances. */
private fun navigateToTasks() {
waitForTag(AccessibilityIds.Navigation.tasksTab)
composeRule.onNodeWithTag(
AccessibilityIds.Navigation.tasksTab,
useUnmergedTree = true,
).performClick()
}
private fun navigateToContractors() {
waitForTag(AccessibilityIds.Navigation.contractorsTab)
composeRule.onNodeWithTag(
AccessibilityIds.Navigation.contractorsTab,
useUnmergedTree = true,
).performClick()
}
private fun navigateToDocuments() {
waitForTag(AccessibilityIds.Navigation.documentsTab)
composeRule.onNodeWithTag(
AccessibilityIds.Navigation.documentsTab,
useUnmergedTree = true,
).performClick()
}
private fun navigateToResidences() {
waitForTag(AccessibilityIds.Navigation.residencesTab)
composeRule.onNodeWithTag(
AccessibilityIds.Navigation.residencesTab,
useUnmergedTree = true,
).performClick()
}
// MARK: - 1. Validation
/** iOS: test01_cancelTaskCreation */
@Test
fun test01_cancelTaskCreation() {
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).performClick()
// PRECONDITION: task form opened (title field visible).
waitForTag(AccessibilityIds.Task.titleField)
composeRule.onNodeWithTag(
AccessibilityIds.Task.titleField,
useUnmergedTree = true,
).assertIsDisplayed()
// Cancel the form.
waitForTag(AccessibilityIds.Task.formCancelButton)
composeRule.onNodeWithTag(
AccessibilityIds.Task.formCancelButton,
useUnmergedTree = true,
).performClick()
// Verify we're back on the task list (add button reachable again,
// title field gone).
waitForTag(AccessibilityIds.Task.addButton)
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
assert(!nodeExists(AccessibilityIds.Task.titleField)) {
"Task form title field should disappear after cancel"
}
}
// MARK: - 2. View/List
/** iOS: test02_tasksTabExists */
@Test
fun test02_tasksTabExists() {
// Tab bar exists (Tasks tab is how we got here).
composeRule.onNodeWithTag(
AccessibilityIds.Navigation.tasksTab,
useUnmergedTree = true,
).assertIsDisplayed()
// Task add button proves we're on the Tasks tab.
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
}
/** iOS: test03_viewTasksList */
@Test
fun test03_viewTasksList() {
// Verified by the add button existence from setUp.
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
}
/** iOS: test04_addTaskButtonEnabled */
@Test
fun test04_addTaskButtonEnabled() {
// Add button is enabled because the seed residence exists.
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).assertIsEnabled()
}
/** iOS: test05_navigateToAddTask */
@Test
fun test05_navigateToAddTask() {
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).performClick()
waitForTag(AccessibilityIds.Task.titleField)
composeRule.onNodeWithTag(
AccessibilityIds.Task.titleField,
useUnmergedTree = true,
).assertIsDisplayed()
// Save button should be present inside the form.
composeRule.onNodeWithTag(
AccessibilityIds.Task.saveButton,
useUnmergedTree = true,
).assertIsDisplayed()
// Cleanup: dismiss the form.
composeRule.onNodeWithTag(
AccessibilityIds.Task.formCancelButton,
useUnmergedTree = true,
).performClick()
}
// MARK: - 5. Cross-tab Navigation
/** iOS: test08_navigateToContractors */
@Test
fun test08_navigateToContractors() {
navigateToContractors()
waitForTag(AccessibilityIds.Contractor.addButton)
composeRule.onNodeWithTag(
AccessibilityIds.Contractor.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
}
/** iOS: test09_navigateToDocuments */
@Test
fun test09_navigateToDocuments() {
navigateToDocuments()
waitForTag(AccessibilityIds.Document.addButton)
composeRule.onNodeWithTag(
AccessibilityIds.Document.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
}
/** iOS: test10_navigateBetweenTabs */
@Test
fun test10_navigateBetweenTabs() {
navigateToResidences()
waitForTag(AccessibilityIds.Residence.addButton)
composeRule.onNodeWithTag(
AccessibilityIds.Residence.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
navigateToTasks()
waitForTag(AccessibilityIds.Task.addButton)
composeRule.onNodeWithTag(
AccessibilityIds.Task.addButton,
useUnmergedTree = true,
).assertIsDisplayed()
}
// ---------------- DataManager init helper ----------------
private fun isDataManagerInitialized(): Boolean {
return try {
val field = DataManager::class.java.getDeclaredField("_isInitialized")
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
val flow = field.get(DataManager) as kotlinx.coroutines.flow.MutableStateFlow<Boolean>
flow.value
} catch (t: Throwable) {
false
}
}
}

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