package com.tt.honeyDue import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextReplacement import androidx.test.ext.junit.runners.AndroidJUnit4 import com.tt.honeyDue.testing.AccessibilityIds import com.tt.honeyDue.ui.screens.MainTabScreen import com.tt.honeyDue.ui.screens.ResidencesFormPageObject import com.tt.honeyDue.ui.screens.ResidencesListPageObject import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue 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 /** * Suite4 — Comprehensive residence tests. * * Ports `iosApp/HoneyDueUITests/Suite4_ComprehensiveResidenceTests.swift` * 1:1 with matching method names. Each Kotlin method keeps the numeric * prefix of its iOS counterpart so `@FixMethodOrder(NAME_ASCENDING)` * preserves the same execution order. * * These tests exercise the real dev backend via the instrumentation process * (mirroring iOS behavior) — no mocks. The suite depends on seeded accounts * from `AAA_SeedTests` so `testuser` exists with at least one residence. * * **Test ownership**: residence screens only. Other surfaces are covered by * sibling suites (Suite1 auth, Suite5 tasks, Suite7 contractors). */ @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class Suite4_ComprehensiveResidenceTests { @get:Rule val composeRule = createAndroidComposeRule() // Tracks residence names created by UI tests so they can be scrubbed via API in teardown. private val createdResidenceNames: MutableList = mutableListOf() @Before fun setUp() { // Dismiss any lingering form from a previous test (defensive — parallel or // retry runs occasionally leave a residence form open). val form = ResidencesFormPageObject(composeRule) if (form.isDisplayed()) form.tapCancel() // Ensure we're authenticated and on the residences tab. UITestHelpers.loginAsTestUser(composeRule) MainTabScreen(composeRule).tapResidencesTab() val list = ResidencesListPageObject(composeRule) list.waitForLoad() } @After fun tearDown() { createdResidenceNames.clear() UITestHelpers.tearDown(composeRule) } // region Helpers private fun list() = ResidencesListPageObject(composeRule) private fun navigateToResidences() { MainTabScreen(composeRule).tapResidencesTab() list().waitForLoad() } private fun createResidence( name: String, street: String = "123 Test St", city: String = "TestCity", stateProvince: String = "TS", postal: String = "12345", ) { val form = list().tapAddResidence() form.enterName(name) form.fillAddress(street, city, stateProvince, postal) form.tapSave() form.waitForDismiss() createdResidenceNames.add(name) } private fun findResidenceNodeExists(nameSubstring: String, timeoutMs: Long = 15_000L): Boolean = try { composeRule.waitUntil(timeoutMs) { try { composeRule.onNode(hasText(nameSubstring, substring = true), useUnmergedTree = true) .assertExists() true } catch (e: AssertionError) { false } } true } catch (t: Throwable) { false } // endregion // MARK: - 1. Error/Validation Tests @Test fun test01_cannotCreateResidenceWithEmptyName() { val form = list().tapAddResidence() // Leave name blank, fill only address. form.fillAddress(street = "123 Test St", city = "TestCity", stateProvince = "TS", postal = "12345") // Save button must be disabled while name is empty. form.assertSaveDisabled() // Clean up so the next test starts on the list. form.tapCancel() } @Test fun test02_cancelResidenceCreation() { val form = list().tapAddResidence() form.enterName("This will be canceled") form.tapCancel() // Back on residences tab — tab bar tag should exist. assertTrue( "Should be back on residences list", composeRule.onNodeWithTagExists(AccessibilityIds.Navigation.residencesTab), ) // Canceled residence must not appear in the list. assertFalse( "Canceled residence should not exist in list", findResidenceNodeExists("This will be canceled", timeoutMs = 3_000L), ) } // MARK: - 2. Creation Tests @Test fun test03_createResidenceWithMinimalData() { val name = uniqueName("Minimal Home") createResidence(name = name) navigateToResidences() assertTrue("Residence should appear in list", findResidenceNodeExists(name)) } // test04 skipped on iOS too — no seeded residence types. @Test fun test05_createMultipleResidencesInSequence() { val ts = System.currentTimeMillis() for (i in 1..3) { val name = "Sequential Home $i - $ts" createResidence(name = name) navigateToResidences() } for (i in 1..3) { val name = "Sequential Home $i - $ts" assertTrue("Residence $i should exist in list", findResidenceNodeExists(name)) } } @Test fun test06_createResidenceWithVeryLongName() { val longName = uniqueName( "This is an extremely long residence name that goes on and on and on to test how the system handles very long text input in the name field", ) createResidence(name = longName) navigateToResidences() assertTrue( "Long name residence should exist", findResidenceNodeExists("extremely long residence"), ) } @Test fun test07_createResidenceWithSpecialCharacters() { val name = uniqueName("Special !@#\$%^&*() Home") createResidence(name = name) navigateToResidences() assertTrue( "Residence with special chars should exist", findResidenceNodeExists("Special"), ) } @Test fun test08_createResidenceWithEmojis() { // Matches iOS text ("Beach House") — no emoji literal in payload to avoid // flaky text matching when some platforms render emoji variants. val name = uniqueName("Beach House") createResidence(name = name) navigateToResidences() assertTrue( "Residence with 'Beach House' label should exist", findResidenceNodeExists("Beach House"), ) } @Test fun test09_createResidenceWithInternationalCharacters() { val name = uniqueName("Chateau Montreal") createResidence(name = name) navigateToResidences() assertTrue( "Residence with international chars should exist", findResidenceNodeExists("Chateau"), ) } @Test fun test10_createResidenceWithVeryLongAddress() { val name = uniqueName("Long Address Home") createResidence( name = name, street = "123456789 Very Long Street Name That Goes On And On Boulevard Apartment Complex Unit 42B", city = "VeryLongCityNameThatTestsTheLimit", stateProvince = "CA", postal = "12345-6789", ) navigateToResidences() assertTrue( "Residence with long address should exist", findResidenceNodeExists(name), ) } // MARK: - 3. Edit/Update Tests @Test fun test11_editResidenceName() { val originalName = uniqueName("Original Name") val newName = uniqueName("Edited Name") createResidence(name = originalName) navigateToResidences() val detail = list().openResidence(originalName) val form = detail.tapEdit() form.replaceName(newName) form.tapSave() form.waitForDismiss() createdResidenceNames.add(newName) navigateToResidences() assertTrue( "Residence should show updated name", findResidenceNodeExists(newName), ) } @Test fun test12_updateAllResidenceFields() { val originalName = uniqueName("Update All Fields") val newName = uniqueName("All Fields Updated") createResidence( name = originalName, street = "123 Old St", city = "OldCity", stateProvince = "OC", postal = "11111", ) navigateToResidences() val detail = list().openResidence(originalName) val form = detail.tapEdit() form.replaceName(newName) // Replace address fields directly via the compose rule. FormTextField has // no clear helper — performTextReplacement handles it without dismissKeyboard // gymnastics. composeRule.onNodeWithTag(AccessibilityIds.Residence.streetAddressField, useUnmergedTree = true) .performTextReplacement("999 Updated Avenue") composeRule.onNodeWithTag(AccessibilityIds.Residence.cityField, useUnmergedTree = true) .performTextReplacement("NewCity") composeRule.onNodeWithTag(AccessibilityIds.Residence.stateProvinceField, useUnmergedTree = true) .performTextReplacement("NC") composeRule.onNodeWithTag(AccessibilityIds.Residence.postalCodeField, useUnmergedTree = true) .performTextReplacement("99999") form.tapSave() form.waitForDismiss() createdResidenceNames.add(newName) navigateToResidences() assertTrue( "Residence should show updated name in list", findResidenceNodeExists(newName), ) } // MARK: - 4. View/Navigation Tests @Test fun test13_viewResidenceDetails() { val name = uniqueName("Detail View Test") createResidence(name = name) navigateToResidences() val detail = list().openResidence(name) detail.waitForLoad() // Detail view is marked with AccessibilityIds.Residence.detailView on its Scaffold. assertTrue( "Detail view should display with edit button or detail tag", composeRule.onNodeWithTagExists(AccessibilityIds.Residence.editButton) || composeRule.onNodeWithTagExists(AccessibilityIds.Residence.detailView), ) } @Test fun test14_navigateFromResidencesToOtherTabs() { val tabs = MainTabScreen(composeRule) tabs.tapResidencesTab() tabs.tapTasksTab() assertTrue( "Tasks tab should be visible after selection", composeRule.onNodeWithTagExists(AccessibilityIds.Navigation.tasksTab), ) tabs.tapResidencesTab() assertTrue( "Residences tab should reselect", composeRule.onNodeWithTagExists(AccessibilityIds.Navigation.residencesTab), ) tabs.tapContractorsTab() assertTrue( "Contractors tab should be visible", composeRule.onNodeWithTagExists(AccessibilityIds.Navigation.contractorsTab), ) tabs.tapResidencesTab() assertTrue( "Residences tab should reselect after contractors", composeRule.onNodeWithTagExists(AccessibilityIds.Navigation.residencesTab), ) } @Test fun test15_refreshResidencesList() { // Android relies on PullToRefreshBox; the UI test harness cannot reliably // gesture a pull-to-refresh, so the test verifies we're still on the // residences tab after re-selecting it (mirrors iOS fallback path). navigateToResidences() MainTabScreen(composeRule).tapResidencesTab() assertTrue( "Should still be on Residences tab after refresh", composeRule.onNodeWithTagExists(AccessibilityIds.Navigation.residencesTab), ) } // MARK: - 5. Persistence Tests @Test fun test16_residencePersistsAfterBackgroundingApp() { val name = uniqueName("Persistence Test") createResidence(name = name) navigateToResidences() assertTrue("Residence should exist before backgrounding", findResidenceNodeExists(name)) // Android equivalent of "background and reactivate": waitForIdle is all the // Compose test harness supports cleanly. The real backgrounding path is // covered by MainActivity lifecycle tests elsewhere. composeRule.waitForIdle() navigateToResidences() assertTrue("Residence should persist after backgrounding", findResidenceNodeExists(name)) } // region Private private fun uniqueName(base: String): String = "$base ${System.currentTimeMillis()}" // endregion } /** * Non-throwing probe for a semantics node with the given test tag. The Compose * Test matcher throws an AssertionError when missing; JUnit would treat that * as a hard failure, so tests use this helper for probe-style checks instead. */ private fun ComposeTestRule.onNodeWithTagExists(testTag: String): Boolean = try { onNodeWithTag(testTag, useUnmergedTree = true).assertExists() true } catch (e: AssertionError) { false }