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() @Before fun setUp() { val context = ApplicationProvider.getApplicationContext() 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 flow.value } catch (t: Throwable) { false } } }