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.unspecced.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.unspecced.getFollowers") assertEquals(alice, call.params["actor"]) assertEquals(harness.did, call.params["viewer"]) } @Test fun followingHasItsOwnEndpointAndWording() { harness.server.on( "social.grain.unspecced.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.unspecced.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.unspecced.getKnownFollowers")) } @Test fun galleryFavoritesTakeAGalleryUriAsTheirSubject() { val gallery = "at://did:plc:alice/social.grain.gallery/g1" harness.server.on( "social.grain.unspecced.getGalleryFavorites", """{"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.unspecced.getGalleryFavorites").params["gallery"], ) } @Test fun asecondPageIsAppendedRatherThanReplacing() { harness.server.on( "social.grain.unspecced.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.unspecced.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.unspecced.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.unspecced.getFollowers")) } @Test fun aFailedLoadReportsItsMessageAndCanBeRetried() { harness.server.on("social.grain.unspecced.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.unspecced.getFollowers", """{"items":[${actors("did:plc:bob")}]}""") model.reload() model.state.awaitValue("the retry") { it.items.isNotEmpty() } } }