~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
108 lines · 3.8 KB Raw History
package social.grain

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.data.api.XrpcClient
import social.grain.data.api.getFeed
import social.grain.data.auth.AuthManager
import social.grain.data.model.GrainGallery
import social.grain.ui.components.GalleryCard
import social.grain.ui.theme.GrainTheme
import java.util.concurrent.TimeUnit

/**
 * Renders real galleries through the real card.
 *
 * Feed data is messier than any fixture: portrait and landscape mixed in one
 * gallery, missing titles, null fav counts, single-photo posts. Composing live
 * responses catches the layout assumptions a hand-written sample would satisfy
 * by accident — an `aspectRatio` of zero crashes the whole feed, not one card.
 */
@RunWith(AndroidJUnit4::class)
class GalleryCardRenderTest {

    @get:Rule
    val composeRule = createComposeRule()

    private val client = XrpcClient(
        baseUrl = AuthManager.SERVER_URL,
        http = OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .build(),
        json = Json {
            ignoreUnknownKeys = true
            explicitNulls = false
            coerceInputValues = true
        },
    )

    private fun liveGalleries(limit: Int): List<GrainGallery> = runBlocking {
        client.getFeed(feed = "recent", limit = limit).items.orEmpty()
    }

    @Test
    fun rendersAFeedOfLiveGalleriesWithoutCrashing() {
        val galleries = liveGalleries(20)
        assertTrue("No galleries to render", galleries.isNotEmpty())

        composeRule.setContent {
            GrainTheme {
                LazyColumn(Modifier.fillMaxSize()) {
                    items(galleries, key = { it.uri }) { GalleryCard(gallery = it) }
                }
            }
        }
        composeRule.waitForIdle()

        // Every card draws a favorite control, so finding one proves the first
        // card composed all the way through rather than bailing early.
        val favoriteNodes = composeRule.onAllNodesWithContentDescription("Favorite")
            .fetchSemanticsNodes()
        val unfavoriteNodes = composeRule.onAllNodesWithContentDescription("Unfavorite")
            .fetchSemanticsNodes()
        assertTrue(
            "No engagement row rendered — the card didn't compose",
            favoriteNodes.isNotEmpty() || unfavoriteNodes.isNotEmpty(),
        )
    }

    @Test
    fun rendersTheAuthorHandleOnTheFirstCard() {
        val gallery = liveGalleries(1).first()

        composeRule.setContent {
            GrainTheme { GalleryCard(gallery = gallery) }
        }
        composeRule.waitForIdle()

        val name = gallery.creator.displayName?.takeIf { it.isNotBlank() }
            ?: gallery.creator.handle
        composeRule.onNodeWithText(name).assertExists()
    }

    @Test
    fun aGalleryWithNoPhotosStillRenders() {
        // The appview can return a gallery whose photos are all deleted; the
        // card must degrade to header-plus-caption rather than throw.
        val stripped = liveGalleries(1).first().copy(items = emptyList())

        composeRule.setContent {
            GrainTheme { GalleryCard(gallery = stripped) }
        }
        composeRule.waitForIdle()
    }
}