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 <noreply@anthropic.com>
This commit is contained in:
grakovne
2026-07-05 17:51:57 +03:00
parent 3d87ba4d2a
commit 6d8dacd2ac
10 changed files with 234 additions and 177 deletions

View File

@@ -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<AppActivity>()
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() {

View File

@@ -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<String, Any?>? = 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<String>().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() }
}
}
}

View File

@@ -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

View File

@@ -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<AppActivity>()
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()
}
}

View File

@@ -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<AppActivity>()
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]

View File

@@ -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<AppActivity>()
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()
}

View File

@@ -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<AppActivity>()
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()
}

View File

@@ -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()

View File

@@ -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<AppActivity>()
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() {

View File

@@ -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<AppActivity>()
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