From 6d8dacd2aca447fd664fffa76279573aaaf8d2bb Mon Sep 17 00:00:00 2001 From: grakovne Date: Sun, 5 Jul 2026 17:51:57 +0300 Subject: [PATCH] Stabilize instrumented E2E tests Share one UI login per run via E2ESession preferences snapshot to stay under the Audiobookshelf login rate limit (40 requests per 10 minutes), make book list waits resilient with scroll-to-node and pull-to-refresh retries, and read the test server host, credentials and search query from instrumentation arguments. Logged-out landscape checks moved to LandscapeLoginE2ETest since the session-backed classes now start on the library screen. Verified with 10/10 consecutive green runs of the full suite (784 unit + 121 instrumented tests). Co-Authored-By: Claude Fable 5 --- .../grakovne/lissen/ui/ConfigBackupE2ETest.kt | 13 +- .../org/grakovne/lissen/ui/E2ETestConfig.kt | 130 +++++++++++++++++- .../grakovne/lissen/ui/LandscapeE2ETest.kt | 54 +------- .../lissen/ui/LandscapeLoginE2ETest.kt | 80 +++++++++++ .../org/grakovne/lissen/ui/LibraryE2ETest.kt | 26 +--- .../lissen/ui/LibrarySearchE2ETest.kt | 28 +--- .../grakovne/lissen/ui/LinkedSearchE2ETest.kt | 32 +---- .../grakovne/lissen/ui/LoginFlowE2ETest.kt | 10 +- .../org/grakovne/lissen/ui/PlayerE2ETest.kt | 26 +--- .../org/grakovne/lissen/ui/SettingsE2ETest.kt | 12 +- 10 files changed, 234 insertions(+), 177 deletions(-) create mode 100644 app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeLoginE2ETest.kt diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/ConfigBackupE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/ConfigBackupE2ETest.kt index 9915e7bf..bfc5b906 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/ConfigBackupE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/ConfigBackupE2ETest.kt @@ -6,10 +6,8 @@ import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.GrantPermissionRule import dagger.hilt.android.testing.HiltAndroidRule @@ -42,6 +40,7 @@ class ConfigBackupE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() } } @@ -49,15 +48,7 @@ class ConfigBackupE2ETest { val composeRule = createAndroidComposeRule() private fun login() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary() } private fun navigateToGeneralAdvancedSettings() { diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/E2ETestConfig.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/E2ETestConfig.kt index fad6cb46..7f9fdb64 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/E2ETestConfig.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/E2ETestConfig.kt @@ -1,6 +1,130 @@ package org.grakovne.lissen.ui -internal const val DEMO_HOST = "https://demo.lissenapp.org" -internal const val DEMO_USERNAME = "demo" -internal const val DEMO_PASSWORD = "demo" +import android.content.Context +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.ComposeTimeoutException +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.core.content.edit +import androidx.test.platform.app.InstrumentationRegistry + +internal val E2E_HOST: String + get() = InstrumentationRegistry.getArguments().getString("e2eHost") ?: "https://demo.lissenapp.org" + +internal val E2E_USERNAME: String + get() = InstrumentationRegistry.getArguments().getString("e2eUsername") ?: "demo" + +internal val E2E_PASSWORD: String + get() = InstrumentationRegistry.getArguments().getString("e2ePassword") ?: "demo" + +internal val E2E_SEARCH_QUERY: String + get() = InstrumentationRegistry.getArguments().getString("e2eSearchQuery") ?: "a" + internal const val TIMEOUT_MS = 45_000L + +internal val bookItemMatcher = + SemanticsMatcher("hasBookItemTag") { node -> + node.config + .getOrElseNullable(SemanticsProperties.TestTag) { null } + ?.startsWith("bookItem_") == true + } + +internal object E2ESession { + private var snapshot: Map? = null + + val available: Boolean + get() = snapshot != null + + fun capture() { + snapshot = securePrefs().all.toMap() + } + + fun restore() { + val data = snapshot ?: return + + securePrefs().edit(commit = true) { + data.forEach { (key, value) -> + when (value) { + is String -> putString(key, value) + is Boolean -> putBoolean(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Float -> putFloat(key, value) + is Set<*> -> putStringSet(key, value.filterIsInstance().toSet()) + } + } + } + } + + private fun securePrefs() = + InstrumentationRegistry + .getInstrumentation() + .targetContext + .getSharedPreferences("secure_prefs", Context.MODE_PRIVATE) +} + +@OptIn(ExperimentalTestApi::class) +internal fun ComposeTestRule.loginToLibrary(scrollToButton: Boolean = false) { + if (E2ESession.available) { + waitUntilAtLeastOneExists( + matcher = hasTestTag("libraryScreen"), + timeoutMillis = TIMEOUT_MS, + ) + return + } + + onNodeWithTag("hostInput").performTextInput(E2E_HOST) + onNodeWithTag("usernameInput").performTextInput(E2E_USERNAME) + onNodeWithTag("passwordInput").performTextInput(E2E_PASSWORD) + + val loginButton = onNodeWithTag("loginButton") + if (scrollToButton) { + loginButton.performScrollTo() + } + loginButton.performClick() + + waitUntilAtLeastOneExists( + matcher = hasTestTag("libraryScreen"), + timeoutMillis = TIMEOUT_MS, + ) + waitUntilBookItemsExist() + + E2ESession.capture() +} + +@OptIn(ExperimentalTestApi::class) +internal fun ComposeTestRule.waitUntilBookItemsExist( + attempts: Int = 3, + attemptTimeoutMillis: Long = 15_000L, +) { + repeat(attempts) { attempt -> + try { + waitUntilAtLeastOneExists(bookItemMatcher, attemptTimeoutMillis) + return + } catch (exception: ComposeTimeoutException) { + val revealedByScroll = + runCatching { + onNodeWithTag("libraryGrid").performScrollToNode(bookItemMatcher) + }.isSuccess + + if (revealedByScroll) { + return + } + + if (attempt == attempts - 1) { + throw exception + } + + onNodeWithTag("libraryGrid").performTouchInput { swipeDown() } + } + } +} diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeE2ETest.kt index f0bd3ad7..52041aa8 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeE2ETest.kt @@ -3,17 +3,13 @@ package org.grakovne.lissen.ui import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration -import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule @@ -33,13 +29,6 @@ import org.junit.runners.MethodSorters import javax.inject.Inject import kotlin.math.abs -private val bookItemMatcher = - SemanticsMatcher("hasBookItemTag") { node -> - node.config - .getOrElseNullable(SemanticsProperties.TestTag) { null } - ?.startsWith("bookItem_") == true - } - @OptIn(ExperimentalTestApi::class) @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -64,6 +53,7 @@ class LandscapeE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() InstrumentationRegistry.getInstrumentation().runOnMainSync { mediaRepository.clearPlayingBook() } @@ -88,22 +78,11 @@ class LandscapeE2ETest { } private fun login() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performScrollTo().performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary(scrollToButton = true) } private fun openFirstBook() { - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.waitUntilBookItemsExist() composeRule.onAllNodes(bookItemMatcher)[0].performClick() @@ -113,28 +92,6 @@ class LandscapeE2ETest { ) } - @Test - fun landscape_loginScreenRenders() { - rotateToLandscape() - - composeRule.onNodeWithTag("loginScreen").assertIsDisplayed() - composeRule.onNodeWithTag("hostInput").assertIsDisplayed() - } - - @Test - fun landscape_settingsFooterIsVisible() { - rotateToLandscape() - - composeRule.onNodeWithTag("loginSettingsButton").performScrollTo().performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("settingsScreen"), - timeoutMillis = TIMEOUT_MS, - ) - - composeRule.onNodeWithTag("settingsFooter").assertIsDisplayed() - } - @Test fun landscape_libraryRenders() { rotateToLandscape() @@ -170,10 +127,7 @@ class LandscapeE2ETest { fun portrait_libraryUsesSingleColumn() { login() - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.waitUntilBookItemsExist() val first = composeRule.onAllNodes(bookItemMatcher)[0].fetchSemanticsNode().boundsInRoot val second = composeRule.onAllNodes(bookItemMatcher)[1].fetchSemanticsNode().boundsInRoot diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeLoginE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeLoginE2ETest.kt new file mode 100644 index 00000000..082280dd --- /dev/null +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LandscapeLoginE2ETest.kt @@ -0,0 +1,80 @@ +package org.grakovne.lissen.ui + +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences +import org.grakovne.lissen.ui.activity.AppActivity +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExternalResource +import org.junit.runner.RunWith +import javax.inject.Inject + +@OptIn(ExperimentalTestApi::class) +@HiltAndroidTest +@RunWith(AndroidJUnit4::class) +class LandscapeLoginE2ETest { + @get:Rule(order = 0) + val grantPermissionsRule: GrantPermissionRule = + GrantPermissionRule.grant(android.Manifest.permission.POST_NOTIFICATIONS) + + @get:Rule(order = 1) + val hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var preferences: LissenSharedPreferences + + @get:Rule(order = 2) + val setupRule = + object : ExternalResource() { + override fun before() { + hiltRule.inject() + preferences.clearPreferences() + } + } + + @get:Rule(order = 3) + val composeRule = createAndroidComposeRule() + + private fun rotateToLandscape() { + composeRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + + composeRule.waitUntil(TIMEOUT_MS) { + composeRule.activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + } + composeRule.waitForIdle() + } + + @Test + fun landscape_loginScreenRenders() { + rotateToLandscape() + + composeRule.onNodeWithTag("loginScreen").assertIsDisplayed() + composeRule.onNodeWithTag("hostInput").assertIsDisplayed() + } + + @Test + fun landscape_settingsFooterIsVisible() { + rotateToLandscape() + + composeRule.onNodeWithTag("loginSettingsButton").performScrollTo().performClick() + + composeRule.waitUntilAtLeastOneExists( + matcher = hasTestTag("settingsScreen"), + timeoutMillis = TIMEOUT_MS, + ) + + composeRule.onNodeWithTag("settingsFooter").assertIsDisplayed() + } +} diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibraryE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibraryE2ETest.kt index 4abf614b..985d5a0e 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibraryE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibraryE2ETest.kt @@ -1,15 +1,12 @@ package org.grakovne.lissen.ui import android.content.Intent -import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule @@ -26,13 +23,6 @@ import org.junit.runner.RunWith import org.junit.runners.MethodSorters import javax.inject.Inject -private val bookItemMatcher = - SemanticsMatcher("hasBookItemTag") { node -> - node.config - .getOrElseNullable(SemanticsProperties.TestTag) { null } - ?.startsWith("bookItem_") == true - } - @OptIn(ExperimentalTestApi::class) @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -54,6 +44,7 @@ class LibraryE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() } override fun after() { @@ -66,15 +57,7 @@ class LibraryE2ETest { val composeRule = createAndroidComposeRule() private fun login() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary() } @Test @@ -93,10 +76,7 @@ class LibraryE2ETest { fun library_tapBookNavigatesToPlayer() { login() - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.waitUntilBookItemsExist() composeRule .onAllNodes(bookItemMatcher)[0] diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibrarySearchE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibrarySearchE2ETest.kt index d49ac5e2..07886856 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibrarySearchE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LibrarySearchE2ETest.kt @@ -1,10 +1,7 @@ package org.grakovne.lissen.ui -import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag @@ -22,13 +19,6 @@ import org.junit.rules.ExternalResource import org.junit.runner.RunWith import javax.inject.Inject -private val bookItemMatcher = - SemanticsMatcher("hasBookItemTag") { node -> - node.config - .getOrElseNullable(SemanticsProperties.TestTag) { null } - ?.startsWith("bookItem_") == true - } - @OptIn(ExperimentalTestApi::class) @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -49,6 +39,7 @@ class LibrarySearchE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() } } @@ -56,15 +47,7 @@ class LibrarySearchE2ETest { val composeRule = createAndroidComposeRule() private fun login() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary() } @Test @@ -82,12 +65,9 @@ class LibrarySearchE2ETest { composeRule.onNodeWithContentDescription("Search").performClick() - composeRule.onNodeWithTag("librarySearchField").performTextInput("a") + composeRule.onNodeWithTag("librarySearchField").performTextInput(E2E_SEARCH_QUERY) - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.waitUntilBookItemsExist() composeRule.onAllNodes(bookItemMatcher)[0].assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LinkedSearchE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LinkedSearchE2ETest.kt index 1726399f..3ff9b52d 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LinkedSearchE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LinkedSearchE2ETest.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule @@ -27,13 +26,6 @@ import org.junit.rules.ExternalResource import org.junit.runner.RunWith import javax.inject.Inject -private val bookItemMatcher = - SemanticsMatcher("hasBookItemTag") { node -> - node.config - .getOrElseNullable(SemanticsProperties.TestTag) { null } - ?.startsWith("bookItem_") == true - } - private val nonEmptySearchFieldMatcher = SemanticsMatcher("hasNonEmptyEditableText") { node -> node.config @@ -65,6 +57,7 @@ class LinkedSearchE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() InstrumentationRegistry.getInstrumentation().runOnMainSync { mediaRepository.clearPlayingBook() } @@ -80,20 +73,8 @@ class LinkedSearchE2ETest { val composeRule = createAndroidComposeRule() private fun openBookDetails() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) - - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary() + composeRule.waitUntilBookItemsExist() composeRule.onAllNodes(bookItemMatcher)[0].performClick() @@ -116,7 +97,6 @@ class LinkedSearchE2ETest { composeRule.onNodeWithTag("linkedSearchAuthor").performClick() - // The library opens directly in search mode with the author pre-filled as the query. composeRule.waitUntilAtLeastOneExists( matcher = hasTestTag("librarySearchField"), timeoutMillis = TIMEOUT_MS, @@ -124,11 +104,7 @@ class LinkedSearchE2ETest { composeRule.onNodeWithTag("librarySearchField").assertIsDisplayed() composeRule.onNodeWithTag("librarySearchField").assert(nonEmptySearchFieldMatcher) - // And the pre-filled query produces results. - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.waitUntilBookItemsExist() composeRule.onAllNodes(bookItemMatcher)[0].assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LoginFlowE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LoginFlowE2ETest.kt index 151cb564..188bb873 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LoginFlowE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/LoginFlowE2ETest.kt @@ -63,9 +63,9 @@ class LoginFlowE2ETest { @Test fun loginWithValidCredentials_navigatesToLibrary() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) + composeRule.onNodeWithTag("hostInput").performTextInput(E2E_HOST) + composeRule.onNodeWithTag("usernameInput").performTextInput(E2E_USERNAME) + composeRule.onNodeWithTag("passwordInput").performTextInput(E2E_PASSWORD) composeRule.onNodeWithTag("loginButton").performClick() composeRule.waitUntilAtLeastOneExists( @@ -78,8 +78,8 @@ class LoginFlowE2ETest { @Test fun loginWithWrongPassword_staysOnLoginScreen() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) + composeRule.onNodeWithTag("hostInput").performTextInput(E2E_HOST) + composeRule.onNodeWithTag("usernameInput").performTextInput(E2E_USERNAME) composeRule.onNodeWithTag("passwordInput").performTextInput("wrong_password_xyz") composeRule.onNodeWithTag("loginButton").performClick() diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/PlayerE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/PlayerE2ETest.kt index 45d7b6ad..e92e27bb 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/PlayerE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/PlayerE2ETest.kt @@ -3,14 +3,12 @@ package org.grakovne.lissen.ui import android.content.Intent import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToIndex -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule @@ -30,13 +28,6 @@ import org.junit.runner.RunWith import org.junit.runners.MethodSorters import javax.inject.Inject -private val bookItemMatcher = - SemanticsMatcher("hasBookItemTag") { node -> - node.config - .getOrElseNullable(SemanticsProperties.TestTag) { null } - ?.startsWith("bookItem_") == true - } - @OptIn(ExperimentalTestApi::class) @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -61,6 +52,7 @@ class PlayerE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() InstrumentationRegistry.getInstrumentation().runOnMainSync { mediaRepository.clearPlayingBook() } @@ -76,20 +68,8 @@ class PlayerE2ETest { val composeRule = createAndroidComposeRule() private fun login() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) - - composeRule.waitUntilAtLeastOneExists( - matcher = bookItemMatcher, - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary() + composeRule.waitUntilBookItemsExist() } private fun loginAndOpenBook() { diff --git a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/SettingsE2ETest.kt b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/SettingsE2ETest.kt index 85c6e5bb..d72e0cba 100644 --- a/app/src/androidTest/kotlin/org/grakovne/lissen/ui/SettingsE2ETest.kt +++ b/app/src/androidTest/kotlin/org/grakovne/lissen/ui/SettingsE2ETest.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.GrantPermissionRule import dagger.hilt.android.testing.HiltAndroidRule @@ -43,6 +42,7 @@ class SettingsE2ETest { override fun before() { hiltRule.inject() preferences.clearPreferences() + E2ESession.restore() } } @@ -50,15 +50,7 @@ class SettingsE2ETest { val composeRule = createAndroidComposeRule() private fun login() { - composeRule.onNodeWithTag("hostInput").performTextInput(DEMO_HOST) - composeRule.onNodeWithTag("usernameInput").performTextInput(DEMO_USERNAME) - composeRule.onNodeWithTag("passwordInput").performTextInput(DEMO_PASSWORD) - composeRule.onNodeWithTag("loginButton").performClick() - - composeRule.waitUntilAtLeastOneExists( - matcher = hasTestTag("libraryScreen"), - timeoutMillis = TIMEOUT_MS, - ) + composeRule.loginToLibrary() } @Test