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

grain.social/grain-android

1 collaborator
0
1
main
190 lines · 6.2 KB Raw History
package social.grain

import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
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.feature.feed.FeedScreen
import social.grain.feature.feed.FeedViewModel
import social.grain.support.Fixtures
import social.grain.support.Harness
import social.grain.support.awaitContentDescription
import social.grain.support.awaitText
import social.grain.support.MainDispatcherRule
import social.grain.support.awaitCall
import social.grain.support.json
import social.grain.ui.theme.GrainTheme

/**
 * The feed as a reader sees it: the pin picker, the two composers behind the
 * +, and the empty state.
 *
 * The empty state matters more than it looks — an account that follows nobody
 * sees it on every launch, and "Nothing here yet" over a feed that simply
 * failed to load is the difference between a prompt and a lie.
 */
@RunWith(AndroidJUnit4::class)
class FeedScreenTest {

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    @get:Rule
    val composeRule = createComposeRule()

    private lateinit var harness: Harness

    @Before
    fun setUp() {
        harness = Harness()
        runBlocking { harness.feedCache.purge(harness.did) }
        harness.server
            .on("social.grain.actor.getPreferences", """{"preferences":[]}""")
            .on("social.grain.story.getAuthors", """{"authors":[]}""")
            .on("social.grain.feed.getFeed", """{"items":[${json(Fixtures.gallery())}]}""")
    }

    @After
    fun tearDown() {
        runBlocking { harness.feedCache.purge(harness.did) }
        harness.tearDown()
    }

    private fun show(
        onOpenProfile: (String) -> Unit = {},
        onCreate: () -> Unit = {},
        onOpenStories: (String) -> Unit = {},
        onCreateStory: () -> Unit = {},
        onOpenHashtag: (String) -> Unit = {},
        onOpenLocation: (String, String) -> Unit = { _, _ -> },
    ) {
        val model = FeedViewModel(
            client = harness.client,
            auth = harness.auth,
            feedCache = harness.feedCache,
            viewedStories = harness.viewedStories,
            storyStatusCache = harness.storyStatus,
            labelDefsCache = harness.labelDefinitions,
        )
        composeRule.setContent {
            GrainTheme {
                FeedScreen(
                    onOpenProfile = onOpenProfile,
                    onCreate = onCreate,
                    onOpenStories = onOpenStories,
                    onCreateStory = onCreateStory,
                    onOpenFavorites = {},
                    onOpenHashtag = onOpenHashtag,
                    onOpenLocation = onOpenLocation,
                    viewModel = model,
                )
            }
        }
        composeRule.waitForIdle()
    }

    @Test
    fun theFeedIsNamedAfterThePinItIsShowing() {
        show()
        composeRule.awaitText("Recent")
    }

    @Test
    fun aLoadedFeedDrawsItsCards() {
        show()
        composeRule.awaitText("A walk in the fog")
        composeRule.awaitText("@alice.grain.social")
    }

    @Test
    fun anEmptyFeedPromptsRatherThanShowingNothing() {
        harness.server.on("social.grain.feed.getFeed", """{"items":[]}""")
        show()

        composeRule.awaitText("Nothing here yet")
        composeRule.onNodeWithText("Follow some photographers, or post the first gallery.")
            .assertExists()

        val before = harness.server.countOf("social.grain.feed.getFeed")
        composeRule.onNodeWithText("Retry").performClick()
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.feed.getFeed") > before }
    }

    @Test
    fun thePinPickerListsEveryPinnedFeedAndSwitchesToTheOneTapped() {
        harness.server.on(
            "social.grain.actor.getPreferences",
            """{"preferences":[{"${'$'}type":"social.grain.actor.defs#pinnedFeedsPref","items":[
                {"id":"recent","label":"Recent","type":"feed","path":"/"},
                {"id":"following","label":"Following","type":"feed","path":"/f"}
            ]}]}""",
        )
        show()

        composeRule.awaitContentDescription("Switch feed")
        composeRule.onNodeWithText("Recent").performClick()
        composeRule.onNodeWithText("Following").performClick()
        composeRule.waitForIdle()

        assertEquals(
            "following",
            harness.server.lastCall("social.grain.feed.getFeed").params["feed"]?.substringAfterLast('/'),
        )
    }

    @Test
    fun anAccountWithNoStoredPinsGetsTheThreeGrainShipsWith() {
        show()

        composeRule.onNodeWithText("Recent").performClick()
        composeRule.awaitText("Following")
        composeRule.awaitText("For you")
    }

    @Test
    fun bothComposersHangOffTheOnePlus() {
        var galleries = 0
        var stories = 0
        show(onCreate = { galleries += 1 }, onCreateStory = { stories += 1 })

        composeRule.onNodeWithContentDescription("Create").performClick()
        composeRule.awaitText("New story")
        composeRule.onNodeWithText("New gallery").performClick()
        assertEquals(1, galleries)

        composeRule.onNodeWithContentDescription("Create").performClick()
        composeRule.onNodeWithText("New story").performClick()
        assertEquals(1, stories)
    }

    @Test
    fun tappingACardsAuthorOpensTheirProfile() {
        var opened: String? = null
        show(onOpenProfile = { opened = it })
        composeRule.awaitText("Alice")

        composeRule.onNodeWithText("Alice").performClick()

        assertEquals("did:plc:alice", opened)
    }

    @Test
    fun favoritingFromTheFeedWritesTheRecord() {
        harness.server.on("com.atproto.repo.createRecord", """{"uri":"at://did:plc:me/fav/1"}""")
        show()

        composeRule.onNodeWithContentDescription("Favorite").performScrollTo().performClick()

        composeRule.waitUntil(10_000) { harness.server.countOf("com.atproto.repo.createRecord") >= 1 }
    }

}