test(android): instrumented-проверка сохранения сортировки
AlbumSortE2ETest на эмуляторе: два альбома, выбор «по названию», включение обратного порядка, перезапуск экрана — порядок и отметки сохраняются.
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user