test(android): instrumented-проверка сохранения сортировки

AlbumSortE2ETest на эмуляторе: два альбома, выбор «по названию», включение
обратного порядка, перезапуск экрана — порядок и отметки сохраняются.
This commit is contained in:
iu
2026-10-16 13:27:23 +03:00
parent 3e8a6b87b7
commit 8010ff26f9
@@ -0,0 +1,163 @@
package com.galo.android
import android.os.Build
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
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.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.galo.android.domain.media.MediaPermissions
import com.galo.android.ui.MainActivity
import com.galo.android.ui.TAG_ALL_FILES_PROMPT_DIALOG
import com.galo.android.ui.albums.TAG_ALBUM_CARD_PREFIX
import com.galo.android.ui.albums.TAG_ALBUMS_GRID
import com.galo.android.ui.albums.TAG_ALBUMS_SORT
import com.galo.android.ui.albums.TAG_ALBUMS_SORT_MARKED_PREFIX
import com.galo.android.ui.albums.TAG_ALBUMS_SORT_MARKED_REVERSED
import com.galo.android.ui.albums.TAG_ALBUMS_SORT_OPTION_PREFIX
import com.galo.android.ui.albums.TAG_ALBUMS_SORT_REVERSED
import com.galo.android.ui.auth.TAG_AUTH_EMAIL
import com.galo.android.ui.auth.TAG_AUTH_IDENTIFIER
import com.galo.android.ui.auth.TAG_AUTH_NAME
import com.galo.android.ui.auth.TAG_AUTH_PASSWORD
import com.galo.android.ui.auth.TAG_AUTH_SUBMIT
import com.galo.android.ui.setup.TAG_SETUP_CONNECT
import com.galo.android.ui.setup.TAG_SETUP_INSECURE
import com.galo.android.ui.setup.TAG_SETUP_URL
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runner.RunWith
import org.junit.runners.model.Statement
/**
* The album-list ordering chosen in the UI survives a restart of the screen.
*
* Start the server with `HTTP_ADDR=:8088 ./bin/galo` and run:
* `./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.galo.android.AlbumSortE2ETest`
*/
@RunWith(AndroidJUnit4::class)
class AlbumSortE2ETest {
private val mediaPermissionRule: TestRule = object : TestRule {
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
override fun evaluate() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val pkg = instrumentation.targetContext.packageName
MediaPermissions.required(Build.VERSION.SDK_INT).forEach { permission ->
runCatching { instrumentation.uiAutomation.grantRuntimePermission(pkg, permission) }
}
base.evaluate()
}
}
}
private val compose = createAndroidComposeRule<MainActivity>()
@get:Rule
val rules: TestRule = RuleChain.outerRule(mediaPermissionRule).around(compose)
private fun app() = ApplicationProvider.getApplicationContext<GaloApplication>()
private fun awaitTag(tag: String) {
compose.waitUntil(TIMEOUT) {
compose.onAllNodesWithTag(tag).fetchSemanticsNodes().isNotEmpty()
}
}
private fun awaitAlbumsScreen() {
compose.waitUntil(TIMEOUT) {
compose.onAllNodesWithText("Пока нет альбомов").fetchSemanticsNodes().isNotEmpty() ||
compose.onAllNodesWithTag(TAG_ALBUMS_GRID).fetchSemanticsNodes().isNotEmpty()
}
}
private fun dismissAllFilesPrompt() {
if (compose.onAllNodesWithTag(TAG_ALL_FILES_PROMPT_DIALOG).fetchSemanticsNodes().isNotEmpty()) {
compose.onNodeWithText("Позже").performClick()
}
}
/** Horizontal position of a card; two cards share the first grid row. */
private fun leftOf(albumId: String) =
compose.onNodeWithTag(TAG_ALBUM_CARD_PREFIX + albumId).getUnclippedBoundsInRoot().left
@Test
fun sortChoiceSurvivesRestart() {
val email = "android-sort-${System.currentTimeMillis()}@example.com"
compose.onNodeWithTag(TAG_SETUP_URL).performTextInput(E2E.BASE_URL)
compose.onNodeWithTag(TAG_SETUP_INSECURE).performClick()
compose.onNodeWithTag(TAG_SETUP_CONNECT).performClick()
compose.waitUntil(TIMEOUT) {
compose.onAllNodesWithTag(TAG_AUTH_IDENTIFIER).fetchSemanticsNodes().isNotEmpty()
}
compose.onNodeWithText("Создать аккаунт").performClick()
compose.waitUntil(TIMEOUT) {
compose.onAllNodesWithTag(TAG_AUTH_NAME).fetchSemanticsNodes().isNotEmpty()
}
compose.onNodeWithTag(TAG_AUTH_NAME).performTextInput("Sorter")
compose.onNodeWithTag(TAG_AUTH_EMAIL).performTextInput(email)
compose.onNodeWithTag(TAG_AUTH_PASSWORD).performTextInput("secret123")
compose.onNodeWithTag(TAG_AUTH_SUBMIT).performClick()
awaitAlbumsScreen()
dismissAllFilesPrompt()
val token = app().container.tokenStore.accessToken()
?: error("expected a stored access token after registration")
// Alpha is created first, Zulu later: by recent change Zulu leads, by
// title Alpha leads.
val alpha = E2E.createAlbum(token, "Alpha")
Thread.sleep(1_100)
val zulu = E2E.createAlbum(token, "Zulu")
runBlocking { app().container.albumRepository.refresh() }
awaitTag(TAG_ALBUM_CARD_PREFIX + alpha)
awaitTag(TAG_ALBUM_CARD_PREFIX + zulu)
assertTrue("most recent first expected", leftOf(zulu) < leftOf(alpha))
compose.onNodeWithTag(TAG_ALBUMS_SORT).performClick()
compose.onNodeWithTag(TAG_ALBUMS_SORT_MARKED_PREFIX + "UPDATED", useUnmergedTree = true)
.assertExists()
compose.onNodeWithTag(TAG_ALBUMS_SORT_OPTION_PREFIX + "TITLE").performClick()
compose.waitForIdle()
assertTrue("alphabetical order expected", leftOf(alpha) < leftOf(zulu))
// Reversing the key turns the order around.
compose.onNodeWithTag(TAG_ALBUMS_SORT).performClick()
compose.onNodeWithTag(TAG_ALBUMS_SORT_REVERSED).performClick()
compose.waitForIdle()
assertTrue("reverse order expected", leftOf(zulu) < leftOf(alpha))
// Restarting the screen must keep the saved key and direction.
compose.activityRule.scenario.recreate()
compose.waitForIdle()
awaitTag(TAG_ALBUM_CARD_PREFIX + alpha)
awaitTag(TAG_ALBUM_CARD_PREFIX + zulu)
dismissAllFilesPrompt()
assertTrue("ordering did not survive the restart", leftOf(zulu) < leftOf(alpha))
compose.onNodeWithTag(TAG_ALBUMS_SORT).performClick()
compose.onNodeWithTag(TAG_ALBUMS_SORT_MARKED_PREFIX + "TITLE", useUnmergedTree = true)
.assertExists()
compose.onNodeWithTag(TAG_ALBUMS_SORT_MARKED_REVERSED, useUnmergedTree = true)
.assertExists()
}
private companion object {
const val TIMEOUT = 30_000L
}
}