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

grain.social/grain-android

1 collaborator
0
1
main
173 lines · 6.0 KB Raw History
package social.grain

import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.feature.actors.ActorListMode
import social.grain.feature.actors.ActorListViewModel
import social.grain.support.Harness
import social.grain.support.MainDispatcherRule
import social.grain.support.awaitValue
import social.grain.support.settle

/**
 * The one screen behind four lists.
 *
 * Each mode has its own endpoint and its own empty-state wording, and only
 * three of the four paginate — known-followers is a single capped response, so
 * asking it for more is a request that can never be satisfied.
 */
@RunWith(AndroidJUnit4::class)
class ActorListViewModelTest {

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    private lateinit var harness: Harness

    private val alice = "did:plc:alice"

    private fun actors(vararg dids: String) =
        dids.joinToString(",") { """{"did":"$it","handle":"${it.substringAfterLast(':')}.test"}""" }

    @Before
    fun setUp() {
        harness = Harness()
    }

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

    private fun viewModel(mode: ActorListMode, subject: String = alice) = ActorListViewModel(
        client = harness.client,
        auth = harness.auth,
        savedStateHandle = SavedStateHandle(mapOf("mode" to mode, "subject" to subject)),
    )

    @Test
    fun followersLoadFromTheirOwnEndpointWithTheViewerAttached() {
        harness.server.on(
            "social.grain.graph.getFollowers",
            """{"totalCount":2,"items":[${actors("did:plc:bob", "did:plc:carol")}]}""",
        )
        val model = viewModel(ActorListMode.FOLLOWERS)
        val state = model.state.awaitValue("the list") { it.hasLoaded }

        assertEquals(2, state.items.size)
        assertEquals(2, state.totalCount)
        assertFalse(state.isLoading)
        assertEquals("Followers", state.title)
        assertEquals("No followers", state.emptyTitle)

        val call = harness.server.lastCall("social.grain.graph.getFollowers")
        assertEquals(alice, call.params["actor"])
        assertNull("The viewer is who signs the call", call.params["viewer"])
    }

    @Test
    fun followingHasItsOwnEndpointAndWording() {
        harness.server.on(
            "social.grain.graph.getFollowing",
            """{"items":[${actors("did:plc:bob")}]}""",
        )
        val state = viewModel(ActorListMode.FOLLOWING).state.awaitValue("the list") { it.hasLoaded }

        assertEquals("Following", state.title)
        assertEquals("No following", state.emptyTitle)
        assertEquals("This account isn't following anyone yet.", state.emptyDescription)
    }

    @Test
    fun knownFollowersIsOneCappedResponseWithNothingMoreToAskFor() {
        harness.server.on(
            "social.grain.graph.getKnownFollowers",
            """{"items":[${actors("did:plc:bob")}]}""",
        )
        val model = viewModel(ActorListMode.KNOWN_FOLLOWERS)
        val state = model.state.awaitValue("the list") { it.hasLoaded }

        assertEquals("Followers you know", state.title)
        assertNull(state.totalCount)

        model.loadMore()
        settle()
        assertEquals(1, harness.server.countOf("social.grain.graph.getKnownFollowers"))
    }

    @Test
    fun galleryFavoritesTakeAGalleryUriAsTheirSubject() {
        val gallery = "at://did:plc:alice/social.grain.gallery/g1"
        harness.server.on(
            "social.grain.gallery.getFavorites",
            """{"totalCount":1,"items":[${actors("did:plc:bob")}]}""",
        )
        val state = viewModel(ActorListMode.GALLERY_FAVORITES, subject = gallery)
            .state.awaitValue("the list") { it.hasLoaded }

        assertEquals("Favorited by", state.title)
        assertEquals("No one has favorited this gallery yet.", state.emptyDescription)
        assertEquals(
            gallery,
            harness.server.lastCall("social.grain.gallery.getFavorites").params["gallery"],
        )
    }

    @Test
    fun asecondPageIsAppendedRatherThanReplacing() {
        harness.server.on(
            "social.grain.graph.getFollowers",
            """{"totalCount":2,"items":[${actors("did:plc:bob")}],"cursor":"c1"}""",
        )
        val model = viewModel(ActorListMode.FOLLOWERS)
        model.state.awaitValue("the first page") { it.hasLoaded }

        harness.server.on(
            "social.grain.graph.getFollowers",
            """{"items":[${actors("did:plc:carol")}]}""",
        )
        model.loadMore()

        val state = model.state.awaitValue("the second page") { it.items.size == 2 }
        assertEquals("The count from the first page is kept", 2, state.totalCount)
    }

    @Test
    fun aListWithNoCursorHasNothingMoreToLoad() {
        harness.server.on(
            "social.grain.graph.getFollowers",
            """{"items":[${actors("did:plc:bob")}]}""",
        )
        val model = viewModel(ActorListMode.FOLLOWERS)
        model.state.awaitValue("the list") { it.hasLoaded }

        model.loadMore()
        settle()

        assertEquals(1, harness.server.countOf("social.grain.graph.getFollowers"))
    }

    @Test
    fun aFailedLoadReportsItsMessageAndCanBeRetried() {
        harness.server.on("social.grain.graph.getFollowers", """{"error":"no"}""", code = 500)
        val model = viewModel(ActorListMode.FOLLOWERS)

        val state = model.state.awaitValue("the error") { it.errorMessage != null }
        assertEquals("HTTP error 500", state.errorMessage)
        assertTrue(state.hasLoaded)

        model.dismissError()
        assertNull(model.state.value.errorMessage)

        harness.server.on("social.grain.graph.getFollowers", """{"items":[${actors("did:plc:bob")}]}""")
        model.reload()
        model.state.awaitValue("the retry") { it.items.isNotEmpty() }
    }
}