mirror of
https://github.com/GrakovNe/lissen-android.git
synced 2026-08-01 10:58:32 -04:00
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:
@@ -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() {
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user