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

grain.social/grain-android

1 collaborator
0
1
main
158 lines · 5.4 KB Raw History
package social.grain

import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.data.local.ProfileInvalidation
import social.grain.feature.profile.ProfileFeedSource
import social.grain.feature.profile.ProfileGalleryFeedScreen
import social.grain.feature.profile.ProfileTab
import social.grain.feature.profile.ProfileViewModel
import social.grain.support.Fixtures
import social.grain.support.Harness
import social.grain.support.MainDispatcherRule
import social.grain.support.awaitText
import social.grain.support.awaitValue
import social.grain.support.json
import social.grain.ui.theme.GrainTheme

/**
 * The profile's grid unrolled into cards.
 *
 * What matters is where it opens: the tile that was tapped should be the first
 * card on screen, with nothing above it composed, rather than the top of the
 * list with a scroll still to come.
 */
@RunWith(AndroidJUnit4::class)
class ProfileGalleryFeedScreenTest {

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    @get:Rule
    val composeRule = createComposeRule()

    private lateinit var harness: Harness

    private val titles = listOf("First", "Second", "Third", "Fourth", "Fifth")

    private fun feed(vararg titles: String) = titles
        .mapIndexed { i, title -> json(Fixtures.gallery(rkey = "g$i", title = title)) }
        .joinToString(",", prefix = """{"items":[""", postfix = "]}")

    @Before
    fun setUp() {
        harness = Harness()
        harness.server
            .on("social.grain.actor.getProfile", json(Fixtures.profileDetailed()))
            .on("social.grain.feed.getFeed", feed(*titles.toTypedArray()))
            .on("social.grain.story.getStories", """{"stories":[]}""")
            .on("social.grain.actor.getFavorites", feed("Faved"))
            .on("social.grain.story.getArchive", """{"stories":[]}""")
    }

    @After
    fun tearDown() = harness.tearDown()

    private fun viewModel(did: String? = "did:plc:alice") = ProfileViewModel(
        client = harness.client,
        auth = harness.auth,
        profileInvalidation = ProfileInvalidation(),
        labelDefsCache = harness.labelDefinitions,
        savedStateHandle = SavedStateHandle(
            if (did == null) emptyMap() else mapOf("did" to did),
        ),
    )

    private fun show(
        model: ProfileViewModel,
        uri: String,
        source: ProfileFeedSource = ProfileFeedSource.GALLERIES,
        onBack: () -> Unit = {},
    ) {
        composeRule.setContent {
            GrainTheme {
                ProfileGalleryFeedScreen(
                    uri = uri,
                    source = source,
                    onBack = onBack,
                    onOpenProfile = {},
                    onOpenHashtag = {},
                    viewModel = model,
                )
            }
        }
        composeRule.waitForIdle()
    }

    @Test
    fun theFeedOpensOnTheTappedGalleryNotTheTopOfTheList() {
        val model = viewModel()
        val galleries = model.state.awaitValue("the grid") { it.galleries.size == titles.size }
            .galleries

        show(model, uri = galleries[3].uri)

        composeRule.awaitText("Fourth")
        composeRule.onNodeWithText("Fourth").assertIsDisplayed()
        // Above the fold means not composed at all in a lazy list — the reader
        // landed on the fourth card, not scrolled past the first three.
        composeRule.onAllNodesWithText("First").assertCountEquals(0)
    }

    @Test
    fun theFeedIsTitledWithTheProfilesHandleAndGoesBack() {
        var back = 0
        val model = viewModel()
        val first = model.state.awaitValue("the grid") { it.galleries.isNotEmpty() }
            .galleries.first()

        show(model, uri = first.uri, onBack = { back += 1 })

        composeRule.awaitText("alice.grain.social")
        composeRule.awaitText("First")
        composeRule.onNodeWithContentDescription("Back").performClick()
        assertEquals(1, back)
    }

    @Test
    fun aFeedFromTheFavoritesTabListsTheFavorites() {
        harness.server.on(
            "social.grain.actor.getProfile",
            json(Fixtures.profileDetailed(did = harness.did, handle = harness.handle)),
        )
        val model = viewModel(did = null)
        model.state.awaitValue("the profile") { it.profile != null }
        model.selectTab(ProfileTab.FAVORITES)
        val faved = model.state.awaitValue("the favorites") { it.favoritesLoaded }
            .favorites.single()

        show(model, uri = faved.uri, source = ProfileFeedSource.FAVORITES)

        composeRule.awaitText("Favorites")
        composeRule.awaitText("Faved")
        composeRule.onAllNodesWithText("First").assertCountEquals(0)
    }

    @Test
    fun aTappedGalleryThatIsNoLongerInTheListOpensAtTheTop() {
        val model = viewModel()
        model.state.awaitValue("the grid") { it.galleries.isNotEmpty() }

        show(model, uri = "at://did:plc:alice/social.grain.gallery/gone")

        composeRule.awaitText("First")
    }
}