package com.tt.honeyDue import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import com.tt.honeyDue.testing.AccessibilityIds import com.tt.honeyDue.ui.screens.MainTabScreen import com.tt.honeyDue.ui.screens.Screens /** * Reusable helpers that mirror `iosApp/HoneyDueUITests/UITestHelpers.swift`. * * Each helper drives off [com.tt.honeyDue.testing.AccessibilityIds] so the * same semantic contract holds across iOS and Android. When the production * app changes authentication or nav flow, update these helpers rather than * every individual test. */ object UITestHelpers { /** Default credentials for the seeded "testuser" account (matches iOS). */ const val DEFAULT_TEST_USERNAME = "testuser" const val DEFAULT_TEST_PASSWORD = "TestPass123!" private fun tagNode(rule: ComposeTestRule, testTag: String): SemanticsNodeInteraction = rule.onNodeWithTag(testTag, useUnmergedTree = true) /** Non-throwing existence check for a test-tag semantics node. */ private fun exists(rule: ComposeTestRule, testTag: String): Boolean = try { tagNode(rule, testTag).assertExists() true } catch (e: AssertionError) { false } /** Waits up to [timeoutMs] for a semantics node with [testTag] to exist. */ private fun waitForTag( rule: ComposeTestRule, testTag: String, timeoutMs: Long = 10_000L, ): Boolean = try { rule.waitUntil(timeoutMs) { exists(rule, testTag) } true } catch (e: Throwable) { false } private fun isOnLoginScreen(rule: ComposeTestRule): Boolean = exists(rule, AccessibilityIds.Authentication.usernameField) private fun isLoggedIn(rule: ComposeTestRule): Boolean = exists(rule, AccessibilityIds.Navigation.residencesTab) /** * Logs out if currently signed in. Noop if already on the login screen. */ fun logout(rule: ComposeTestRule) { if (waitForTag(rule, AccessibilityIds.Authentication.usernameField, timeoutMs = 2_000L)) return if (!isLoggedIn(rule)) return val tabs = MainTabScreen(rule) tabs.goToSettings() // Some builds back the logout behind an outer profile tab instead; // either path converges on the `Profile.LogoutButton` test tag. if (waitForTag(rule, AccessibilityIds.Profile.logoutButton, timeoutMs = 5_000L)) { tabs.tapLogout() } // Wait until we transition back to login (15s budget matches iOS). waitForTag(rule, AccessibilityIds.Authentication.usernameField, timeoutMs = 15_000L) } /** * Logs in using the provided credentials. Waits for the main tabs to * appear before returning. Throws if the tab bar never shows up. */ fun loginAsTestUser( rule: ComposeTestRule, username: String = DEFAULT_TEST_USERNAME, password: String = DEFAULT_TEST_PASSWORD, ): MainTabScreen { ensureOnLoginScreen(rule) val tabs = Screens.login(rule).login(username, password) waitForTag(rule, AccessibilityIds.Navigation.residencesTab, timeoutMs = 15_000L) return tabs } /** * Best-effort navigation to the login screen from whatever state the app * is in. If we are already logged in, logs the user out first. */ fun ensureOnLoginScreen(rule: ComposeTestRule) { if (isOnLoginScreen(rule)) return if (isLoggedIn(rule)) { logout(rule) if (isOnLoginScreen(rule)) return } // Onboarding flow: tap the "login" affordance if present. if (exists(rule, AccessibilityIds.Onboarding.loginButton)) { tagNode(rule, AccessibilityIds.Onboarding.loginButton).performClick() } waitForTag(rule, AccessibilityIds.Authentication.usernameField, timeoutMs = 20_000L) } /** * Resets the UI to a known-good starting state. Called from test * teardown so residual state from one test doesn't poison the next. */ fun tearDown(rule: ComposeTestRule) { try { logout(rule) } catch (t: Throwable) { // Swallow — teardown must never fail a test. } } }