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:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,11 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tt.honeyDue.data.DataManager
|
import com.tt.honeyDue.data.DataManager
|
||||||
|
import com.tt.honeyDue.testing.AccessibilityIds
|
||||||
import com.tt.honeyDue.models.TaskTemplate
|
import com.tt.honeyDue.models.TaskTemplate
|
||||||
import com.tt.honeyDue.repository.LookupsRepository
|
import com.tt.honeyDue.repository.LookupsRepository
|
||||||
import com.tt.honeyDue.models.MyResidencesResponse
|
import com.tt.honeyDue.models.MyResidencesResponse
|
||||||
@@ -150,7 +152,8 @@ fun AddTaskDialog(
|
|||||||
label = { Text(stringResource(Res.string.tasks_property_required)) },
|
label = { Text(stringResource(Res.string.tasks_property_required)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor(),
|
.menuAnchor()
|
||||||
|
.testTag(AccessibilityIds.Task.residencePicker),
|
||||||
isError = residenceError,
|
isError = residenceError,
|
||||||
supportingText = if (residenceError) {
|
supportingText = if (residenceError) {
|
||||||
{ Text(stringResource(Res.string.tasks_property_error)) }
|
{ Text(stringResource(Res.string.tasks_property_error)) }
|
||||||
@@ -191,7 +194,9 @@ fun AddTaskDialog(
|
|||||||
showSuggestions = it.length >= 2 && filteredSuggestions.isNotEmpty()
|
showSuggestions = it.length >= 2 && filteredSuggestions.isNotEmpty()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.titleField),
|
||||||
isError = titleError,
|
isError = titleError,
|
||||||
supportingText = if (titleError) {
|
supportingText = if (titleError) {
|
||||||
{ Text(stringResource(Res.string.tasks_title_error)) }
|
{ Text(stringResource(Res.string.tasks_title_error)) }
|
||||||
@@ -216,7 +221,9 @@ fun AddTaskDialog(
|
|||||||
value = description,
|
value = description,
|
||||||
onValueChange = { description = it },
|
onValueChange = { description = it },
|
||||||
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.descriptionField),
|
||||||
minLines = 2,
|
minLines = 2,
|
||||||
maxLines = 4
|
maxLines = 4
|
||||||
)
|
)
|
||||||
@@ -232,7 +239,8 @@ fun AddTaskDialog(
|
|||||||
label = { Text(stringResource(Res.string.tasks_category_required)) },
|
label = { Text(stringResource(Res.string.tasks_category_required)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor(),
|
.menuAnchor()
|
||||||
|
.testTag(AccessibilityIds.Task.categoryPicker),
|
||||||
isError = categoryError,
|
isError = categoryError,
|
||||||
supportingText = if (categoryError) {
|
supportingText = if (categoryError) {
|
||||||
{ Text(stringResource(Res.string.tasks_category_error)) }
|
{ Text(stringResource(Res.string.tasks_category_error)) }
|
||||||
@@ -269,7 +277,8 @@ fun AddTaskDialog(
|
|||||||
label = { Text(stringResource(Res.string.tasks_frequency_label)) },
|
label = { Text(stringResource(Res.string.tasks_frequency_label)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor(),
|
.menuAnchor()
|
||||||
|
.testTag(AccessibilityIds.Task.frequencyPicker),
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showFrequencyDropdown) },
|
||||||
enabled = frequencies.isNotEmpty()
|
enabled = frequencies.isNotEmpty()
|
||||||
@@ -300,7 +309,9 @@ fun AddTaskDialog(
|
|||||||
value = intervalDays,
|
value = intervalDays,
|
||||||
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
|
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
|
||||||
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.intervalDaysField),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
|
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
|
||||||
singleLine = true
|
singleLine = true
|
||||||
@@ -315,7 +326,9 @@ fun AddTaskDialog(
|
|||||||
dueDateError = false
|
dueDateError = false
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(Res.string.tasks_due_date_required)) },
|
label = { Text(stringResource(Res.string.tasks_due_date_required)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.dueDatePicker),
|
||||||
isError = dueDateError,
|
isError = dueDateError,
|
||||||
supportingText = if (dueDateError) {
|
supportingText = if (dueDateError) {
|
||||||
{ Text(stringResource(Res.string.tasks_due_date_format_error)) }
|
{ Text(stringResource(Res.string.tasks_due_date_format_error)) }
|
||||||
@@ -336,7 +349,8 @@ fun AddTaskDialog(
|
|||||||
label = { Text(stringResource(Res.string.tasks_priority_label)) },
|
label = { Text(stringResource(Res.string.tasks_priority_label)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.menuAnchor(),
|
.menuAnchor()
|
||||||
|
.testTag(AccessibilityIds.Task.priorityPicker),
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showPriorityDropdown) },
|
||||||
enabled = priorities.isNotEmpty()
|
enabled = priorities.isNotEmpty()
|
||||||
@@ -362,7 +376,9 @@ fun AddTaskDialog(
|
|||||||
value = estimatedCost,
|
value = estimatedCost,
|
||||||
onValueChange = { estimatedCost = it },
|
onValueChange = { estimatedCost = it },
|
||||||
label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) },
|
label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.estimatedCostField),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
prefix = { Text("$") },
|
prefix = { Text("$") },
|
||||||
singleLine = true
|
singleLine = true
|
||||||
@@ -381,6 +397,7 @@ fun AddTaskDialog(
|
|||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(
|
Button(
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.saveButton),
|
||||||
onClick = {
|
onClick = {
|
||||||
// Validation
|
// Validation
|
||||||
var hasError = false
|
var hasError = false
|
||||||
@@ -433,7 +450,10 @@ fun AddTaskDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton),
|
||||||
|
onClick = onDismiss
|
||||||
|
) {
|
||||||
Text(stringResource(Res.string.common_cancel))
|
Text(stringResource(Res.string.common_cancel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import honeydue.composeapp.generated.resources.*
|
import honeydue.composeapp.generated.resources.*
|
||||||
|
import com.tt.honeyDue.testing.AccessibilityIds
|
||||||
import com.tt.honeyDue.models.TaskDetail
|
import com.tt.honeyDue.models.TaskDetail
|
||||||
import com.tt.honeyDue.models.TaskCategory
|
import com.tt.honeyDue.models.TaskCategory
|
||||||
import com.tt.honeyDue.models.TaskPriority
|
import com.tt.honeyDue.models.TaskPriority
|
||||||
@@ -38,7 +40,9 @@ fun TaskCard(
|
|||||||
onCompletionHistoryClick: (() -> Unit)? = null
|
onCompletionHistoryClick: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.taskCard),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tt.honeyDue.models.TaskColumn
|
import com.tt.honeyDue.models.TaskColumn
|
||||||
import com.tt.honeyDue.models.TaskDetail
|
import com.tt.honeyDue.models.TaskDetail
|
||||||
|
import com.tt.honeyDue.testing.AccessibilityIds
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -275,7 +277,7 @@ fun DynamicTaskKanbanView(
|
|||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize().testTag(AccessibilityIds.Task.kanbanView),
|
||||||
pageSpacing = 16.dp,
|
pageSpacing = 16.dp,
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
|
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
|
||||||
) { page ->
|
) { page ->
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.AddNewTaskWithResidenceDialog
|
||||||
import com.tt.honeyDue.ui.components.ApiResultHandler
|
import com.tt.honeyDue.ui.components.ApiResultHandler
|
||||||
import com.tt.honeyDue.ui.components.CompleteTaskDialog
|
import com.tt.honeyDue.ui.components.CompleteTaskDialog
|
||||||
@@ -120,7 +122,8 @@ fun AllTasksScreen(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.loadTasks(forceRefresh = true) }
|
onClick = { viewModel.loadTasks(forceRefresh = true) },
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.refreshButton)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Refresh,
|
Icons.Default.Refresh,
|
||||||
@@ -130,7 +133,8 @@ fun AllTasksScreen(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showNewTaskDialog = true },
|
onClick = { showNewTaskDialog = true },
|
||||||
enabled = myResidencesState is ApiResult.Success &&
|
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(
|
Icon(
|
||||||
Icons.Default.Add,
|
Icons.Default.Add,
|
||||||
@@ -185,7 +189,9 @@ fun AllTasksScreen(
|
|||||||
OrganicPrimaryButton(
|
OrganicPrimaryButton(
|
||||||
text = "Add Task",
|
text = "Add Task",
|
||||||
onClick = { showNewTaskDialog = true },
|
onClick = { showNewTaskDialog = true },
|
||||||
modifier = Modifier.fillMaxWidth(0.7f),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.7f)
|
||||||
|
.testTag(AccessibilityIds.Task.emptyStateView),
|
||||||
enabled = myResidencesState is ApiResult.Success &&
|
enabled = myResidencesState is ApiResult.Success &&
|
||||||
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
|
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.tt.honeyDue.testing.AccessibilityIds
|
||||||
import honeydue.composeapp.generated.resources.*
|
import honeydue.composeapp.generated.resources.*
|
||||||
import com.tt.honeyDue.models.TaskCompletionCreateRequest
|
import com.tt.honeyDue.models.TaskCompletionCreateRequest
|
||||||
import com.tt.honeyDue.models.ContractorSummary
|
import com.tt.honeyDue.models.ContractorSummary
|
||||||
@@ -87,7 +89,10 @@ fun CompleteTaskScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(
|
||||||
|
onClick = onNavigateBack,
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.detailCancelButton)
|
||||||
|
) {
|
||||||
Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.common_cancel))
|
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)) },
|
label = { Text(stringResource(Res.string.completions_actual_cost_optional)) },
|
||||||
leadingIcon = { Icon(Icons.Default.AttachMoney, null) },
|
leadingIcon = { Icon(Icons.Default.AttachMoney, null) },
|
||||||
prefix = { Text("$") },
|
prefix = { Text("$") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.actualCostField),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
shape = OrganicShapes.medium
|
shape = OrganicShapes.medium
|
||||||
@@ -252,7 +259,8 @@ fun CompleteTaskScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = OrganicSpacing.lg)
|
.padding(horizontal = OrganicSpacing.lg)
|
||||||
.height(120.dp),
|
.height(120.dp)
|
||||||
|
.testTag(AccessibilityIds.Task.notesField),
|
||||||
shape = OrganicShapes.medium
|
shape = OrganicShapes.medium
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -416,7 +424,8 @@ fun CompleteTaskScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = OrganicSpacing.lg),
|
.padding(horizontal = OrganicSpacing.lg)
|
||||||
|
.testTag(AccessibilityIds.Task.submitButton),
|
||||||
enabled = !isSubmitting,
|
enabled = !isSubmitting,
|
||||||
isLoading = isSubmitting,
|
isLoading = isSubmitting,
|
||||||
icon = Icons.Default.CheckCircle
|
icon = Icons.Default.CheckCircle
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.tt.honeyDue.testing.AccessibilityIds
|
||||||
import com.tt.honeyDue.ui.components.HandleErrors
|
import com.tt.honeyDue.ui.components.HandleErrors
|
||||||
import com.tt.honeyDue.viewmodel.ResidenceViewModel
|
import com.tt.honeyDue.viewmodel.ResidenceViewModel
|
||||||
import com.tt.honeyDue.repository.LookupsRepository
|
import com.tt.honeyDue.repository.LookupsRepository
|
||||||
@@ -98,7 +100,10 @@ fun EditTaskScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(Res.string.tasks_edit_title)) },
|
title = { Text(stringResource(Res.string.tasks_edit_title)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(
|
||||||
|
onClick = onNavigateBack,
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton)
|
||||||
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +131,9 @@ fun EditTaskScreen(
|
|||||||
value = title,
|
value = title,
|
||||||
onValueChange = { title = it },
|
onValueChange = { title = it },
|
||||||
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.titleField),
|
||||||
isError = titleError.isNotEmpty(),
|
isError = titleError.isNotEmpty(),
|
||||||
supportingText = if (titleError.isNotEmpty()) {
|
supportingText = if (titleError.isNotEmpty()) {
|
||||||
{ Text(titleError) }
|
{ Text(titleError) }
|
||||||
@@ -137,7 +144,9 @@ fun EditTaskScreen(
|
|||||||
value = description,
|
value = description,
|
||||||
onValueChange = { description = it },
|
onValueChange = { description = it },
|
||||||
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(AccessibilityIds.Task.descriptionField),
|
||||||
minLines = 3,
|
minLines = 3,
|
||||||
maxLines = 5
|
maxLines = 5
|
||||||
)
|
)
|
||||||
@@ -304,6 +313,7 @@ fun EditTaskScreen(
|
|||||||
// Submit button
|
// Submit button
|
||||||
OrganicPrimaryButton(
|
OrganicPrimaryButton(
|
||||||
text = stringResource(Res.string.tasks_update),
|
text = stringResource(Res.string.tasks_update),
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.saveButton),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (validateForm() && selectedCategory != null &&
|
if (validateForm() && selectedCategory != null &&
|
||||||
selectedFrequency != null && selectedPriority != null) {
|
selectedFrequency != null && selectedPriority != null) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -38,6 +39,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.tt.honeyDue.network.ApiResult
|
import com.tt.honeyDue.network.ApiResult
|
||||||
import com.tt.honeyDue.repository.LookupsRepository
|
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.common.StandardCard
|
||||||
import com.tt.honeyDue.ui.components.forms.FormTextField
|
import com.tt.honeyDue.ui.components.forms.FormTextField
|
||||||
import com.tt.honeyDue.ui.theme.AppRadius
|
import com.tt.honeyDue.ui.theme.AppRadius
|
||||||
@@ -88,7 +90,10 @@ fun AddTaskWithResidenceScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("New Task", fontWeight = FontWeight.SemiBold) },
|
title = { Text("New Task", fontWeight = FontWeight.SemiBold) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(
|
||||||
|
onClick = onNavigateBack,
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.formCancelButton)
|
||||||
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -112,6 +117,7 @@ fun AddTaskWithResidenceScreen(
|
|||||||
value = title,
|
value = title,
|
||||||
onValueChange = viewModel::onTitleChange,
|
onValueChange = viewModel::onTitleChange,
|
||||||
label = "Title",
|
label = "Title",
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.titleField),
|
||||||
placeholder = "e.g. Flush water heater",
|
placeholder = "e.g. Flush water heater",
|
||||||
error = titleError,
|
error = titleError,
|
||||||
enabled = !isSubmitting
|
enabled = !isSubmitting
|
||||||
@@ -123,6 +129,7 @@ fun AddTaskWithResidenceScreen(
|
|||||||
value = description,
|
value = description,
|
||||||
onValueChange = viewModel::onDescriptionChange,
|
onValueChange = viewModel::onDescriptionChange,
|
||||||
label = "Description",
|
label = "Description",
|
||||||
|
modifier = Modifier.testTag(AccessibilityIds.Task.descriptionField),
|
||||||
placeholder = "Optional details",
|
placeholder = "Optional details",
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
@@ -242,7 +249,8 @@ fun AddTaskWithResidenceScreen(
|
|||||||
enabled = canSubmit && !isSubmitting,
|
enabled = canSubmit && !isSubmitting,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(52.dp),
|
.height(52.dp)
|
||||||
|
.testTag(AccessibilityIds.Task.saveButton),
|
||||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(AppRadius.md),
|
shape = androidx.compose.foundation.shape.RoundedCornerShape(AppRadius.md),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
|||||||
Reference in New Issue
Block a user