diff --git a/android/app/src/androidTest/java/com/galo/android/AlbumSortE2ETest.kt b/android/app/src/androidTest/java/com/galo/android/AlbumSortE2ETest.kt new file mode 100644 index 0000000..4e73f07 --- /dev/null +++ b/android/app/src/androidTest/java/com/galo/android/AlbumSortE2ETest.kt @@ -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() + + @get:Rule + val rules: TestRule = RuleChain.outerRule(mediaPermissionRule).around(compose) + + private fun app() = ApplicationProvider.getApplicationContext() + + 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 + } +}