Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471package social.grain
import androidx.lifecycle.SavedStateHandleimport androidx.test.ext.junit.runners.AndroidJUnit4import org.junit.Afterimport org.junit.Assert.assertEqualsimport org.junit.Assert.assertFalseimport org.junit.Assert.assertNullimport org.junit.Assert.assertTrueimport org.junit.Beforeimport org.junit.Ruleimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.data.local.ProfileInvalidationimport social.grain.feature.profile.ProfileTabimport social.grain.feature.profile.ProfileViewModelimport social.grain.support.Fixturesimport social.grain.support.Harnessimport social.grain.support.MainDispatcherRuleimport social.grain.support.awaitCallimport social.grain.support.awaitValueimport social.grain.support.jsonimport social.grain.support.settle
/** * A profile screen's state, for the viewer's own account and for someone else's. * * The two cases diverge more than they look: only your own profile has tabs * past the first, only someone else's can be followed, blocked or muted, and * every one of those is flipped optimistically and has to be put back when the * write fails. */@RunWith(AndroidJUnit4::class)class ProfileViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private lateinit var harness: Harness private lateinit var invalidation: ProfileInvalidation
private val alice = "did:plc:alice"
@Before fun setUp() { harness = Harness() invalidation = ProfileInvalidation() harness.server .on("dev.hatk.describeLabels", """{"definitions":[]}""") .on("social.grain.unspecced.getActorProfile", json(Fixtures.profileDetailed())) .on("dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery())}]}""") .on("social.grain.unspecced.getStories", """{"stories":[]}""") .on("social.grain.unspecced.getActorFavorites", """{"items":[]}""") .on("social.grain.unspecced.getStoryArchive", """{"stories":[]}""") }
@After fun tearDown() = harness.tearDown()
private fun viewModel(did: String? = alice) = ProfileViewModel( client = harness.client, auth = harness.auth, profileInvalidation = invalidation, labelDefsCache = harness.labelDefinitions, savedStateHandle = SavedStateHandle( if (did == null) emptyMap() else mapOf("did" to did), ), )
private fun ownProfile() = json( Fixtures.profileDetailed(did = harness.did, handle = harness.handle, displayName = "Me"), )
@Test fun someoneElsesProfileLoadsWithTheirGalleries() { val model = viewModel() val state = model.state.awaitValue("the profile") { it.profile != null }
assertEquals("Alice", state.profile!!.displayName) assertEquals(1, state.galleries.size) assertFalse(state.isLoading) assertFalse(state.isOwnProfile) assertNull(state.errorMessage) assertEquals("The actor feed is what fills the grid", "actor", harness.server.lastCall("dev.hatk.getFeed").params["feed"]) assertEquals(alice, harness.server.lastCall("dev.hatk.getFeed").params["actor"]) }
@Test fun theProfileTabWithNoRouteArgumentIsTheViewersOwn() { harness.server.on("social.grain.unspecced.getActorProfile", ownProfile()) val model = viewModel(did = null) val state = model.state.awaitValue("the profile") { it.profile != null }
assertTrue(state.isOwnProfile) assertEquals(harness.did, state.did) }
@Test fun aLiveStoryPutsARingOnTheAvatar() { harness.server.on( "social.grain.unspecced.getStories", """{"stories":[${json(Fixtures.story())}]}""", ) val model = viewModel() assertTrue(model.state.awaitValue("the ring") { it.hasLiveStory }.hasLiveStory) }
@Test fun anExpiredStoryIsNotALiveOne() { harness.server.on( "social.grain.unspecced.getStories", """{"stories":[${json(Fixtures.story(expired = true))}]}""", ) val model = viewModel() assertFalse(model.state.awaitValue("the profile") { it.profile != null }.hasLiveStory) }
@Test fun aProfileThatFailsToLoadReportsIt() { harness.server.on( "social.grain.unspecced.getActorProfile", """{"error":"gone"}""", code = 404, ) val model = viewModel() val state = model.state.awaitValue("the failure") { !it.isLoading }
assertNull(state.profile) assertEquals("HTTP error 404", state.errorMessage)
model.dismissError() assertNull(model.state.value.errorMessage) }
@Test fun theRelationshipFlagsReadOffTheViewerState() { harness.server.on( "social.grain.unspecced.getActorProfile", json( Fixtures.profileDetailed( following = "at://follow/1", followedBy = "at://follow/2", muted = true, ), ), ) val model = viewModel() val state = model.state.awaitValue("the profile") { it.profile != null }
assertTrue(state.isFollowing) assertTrue(state.followsViewer) assertTrue(state.isMuted) assertFalse(state.isBlocking) assertFalse(state.isBlockHidden) }
@Test fun aBlockInEitherDirectionHidesTheProfile() { harness.server.on( "social.grain.unspecced.getActorProfile", json(Fixtures.profileDetailed(blocking = "at://block/1")), ) val blocking = viewModel().state.awaitValue("the profile") { it.profile != null } assertTrue(blocking.isBlocking) assertTrue(blocking.isBlockHidden) }
@Test fun favoritesLoadOnceTheTabIsOpened() { harness.server.on( "social.grain.unspecced.getActorFavorites", """{"items":[${json(Fixtures.gallery(rkey = "faved", title = "Faved"))}]}""", ) harness.server.on("social.grain.unspecced.getActorProfile", ownProfile()) val model = viewModel(did = harness.did) model.state.awaitValue("the profile") { it.profile != null } assertEquals(0, harness.server.countOf("social.grain.unspecced.getActorFavorites"))
model.selectTab(ProfileTab.FAVORITES)
val state = model.state.awaitValue("the favorites") { it.favoritesLoaded } assertEquals("Faved", state.favorites.single().title) assertEquals(ProfileTab.FAVORITES, state.tab) }
@Test fun theArchiveLoadsOnceItsTabIsOpened() { harness.server.on( "social.grain.unspecced.getStoryArchive", """{"stories":[${json(Fixtures.story(rkey = "old", expired = true))}]}""", ) harness.server.on("social.grain.unspecced.getActorProfile", ownProfile()) val model = viewModel(did = harness.did) model.state.awaitValue("the profile") { it.profile != null }
model.selectTab(ProfileTab.STORIES)
val state = model.state.awaitValue("the archive") { it.archiveLoaded } assertEquals(1, state.archivedStories.size) }
@Test fun anotherAccountsProfileRefusesTheTabsItDoesNotHave() { val model = viewModel() model.state.awaitValue("the profile") { it.profile != null }
model.selectTab(ProfileTab.FAVORITES) settle()
assertEquals(ProfileTab.GALLERIES, model.state.value.tab) assertEquals(0, harness.server.countOf("social.grain.unspecced.getActorFavorites")) }
@Test fun aRefreshRenewsTabsThatHaveBeenOpenedAndNothingElse() { harness.server.on("social.grain.unspecced.getActorProfile", ownProfile()) val model = viewModel(did = harness.did) model.state.awaitValue("the profile") { it.profile != null }
model.selectTab(ProfileTab.FAVORITES) model.state.awaitValue("the favorites") { it.favoritesLoaded } val favoritesBefore = harness.server.countOf("social.grain.unspecced.getActorFavorites")
model.refresh() harness.server.awaitCall("social.grain.unspecced.getActorFavorites", favoritesBefore + 1)
assertEquals( "The archive was never opened, so a refresh doesn't fetch it", 0, harness.server.countOf("social.grain.unspecced.getStoryArchive"), ) }
@Test fun morGalleriesAppendBehindTheCursor() { harness.server.on( "dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery())}],"cursor":"c1"}""", ) val model = viewModel() model.state.awaitValue("the grid") { it.galleries.isNotEmpty() }
harness.server.on( "dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery(rkey = "g2", title = "Second"))}]}""", ) model.loadMoreGalleries()
val state = model.state.awaitValue("the second page") { it.galleries.size == 2 } assertEquals("Second", state.galleries[1].title) }
@Test fun aGridWithNoCursorHasNothingMoreToLoad() { val model = viewModel() model.state.awaitValue("the grid") { it.galleries.isNotEmpty() } val before = harness.server.countOf("dev.hatk.getFeed")
model.loadMoreGalleries() settle()
assertEquals(before, harness.server.countOf("dev.hatk.getFeed")) }
@Test fun moreArchivedStoriesAppendBehindTheirOwnCursor() { harness.server.on("social.grain.unspecced.getActorProfile", ownProfile()) harness.server.on( "social.grain.unspecced.getStoryArchive", """{"stories":[${json(Fixtures.story(rkey = "s1"))}],"cursor":"a1"}""", ) val model = viewModel(did = harness.did) model.state.awaitValue("the profile") { it.profile != null } model.selectTab(ProfileTab.STORIES) model.state.awaitValue("the archive") { it.archiveLoaded }
harness.server.on( "social.grain.unspecced.getStoryArchive", """{"stories":[${json(Fixtures.story(rkey = "s2"))}]}""", ) model.loadMoreArchive()
model.state.awaitValue("the second archive page") { it.archivedStories.size == 2 } }
@Test fun followingFlipsTheButtonThenKeepsTheRecordUri() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/follow/1"}""") val model = viewModel() model.state.awaitValue("the profile") { it.profile != null }
model.toggleFollow()
assertTrue("The button flips first", model.state.value.isFollowing) assertEquals(121, model.state.value.profile!!.followersCount)
val settled = model.state.awaitValue("the record uri") { it.profile?.viewer?.following == "at://did:plc:me/follow/1" } assertFalse(settled.isFollowPending) }
@Test fun unfollowingDeletesTheRecordAndDecrementsTheCount() { harness.server.on( "social.grain.unspecced.getActorProfile", json(Fixtures.profileDetailed(following = "at://did:plc:me/social.grain.graph.follow/f1")), ) val model = viewModel() model.state.awaitValue("the profile") { it.isFollowing }
model.toggleFollow()
assertTrue(harness.server.awaitCall("dev.hatk.deleteRecord").body.contains("\"rkey\":\"f1\"")) val state = model.state.awaitValue("the write to settle") { !it.isFollowPending } assertFalse(state.isFollowing) assertEquals(119, state.profile!!.followersCount) }
@Test fun aFollowThatFailsIsPutBack() { harness.server.on("dev.hatk.createRecord", """{"error":"no"}""", code = 500) val model = viewModel() model.state.awaitValue("the profile") { it.profile != null }
model.toggleFollow()
val state = model.state.awaitValue("the revert") { it.errorMessage != null } assertFalse(state.isFollowing) assertEquals(120, state.profile!!.followersCount) assertFalse(state.isFollowPending) }
@Test fun nobodyCanFollowThemselves() { harness.server.on("social.grain.unspecced.getActorProfile", ownProfile()) val model = viewModel(did = harness.did) model.state.awaitValue("the profile") { it.profile != null }
model.toggleFollow() settle()
assertEquals(0, harness.server.countOf("dev.hatk.createRecord")) }
@Test fun blockingWritesTheRecordAndReloadsTheScreen() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/block/1"}""") val model = viewModel() model.state.awaitValue("the profile") { it.profile != null } val profileFetches = harness.server.countOf("social.grain.unspecced.getActorProfile")
model.toggleBlock()
assertTrue("The state flips first", model.state.value.isBlocking) harness.server.awaitCall("social.grain.unspecced.getActorProfile", profileFetches + 1) }
@Test fun unblockingDeletesTheBlockRecord() { harness.server.on( "social.grain.unspecced.getActorProfile", json(Fixtures.profileDetailed(blocking = "at://did:plc:me/social.grain.graph.block/b1")), ) val model = viewModel() model.state.awaitValue("the profile") { it.isBlocking }
model.toggleBlock()
assertTrue(harness.server.awaitCall("dev.hatk.deleteRecord").body.contains("\"rkey\":\"b1\"")) }
@Test fun aBlockThatFailsIsPutBackWithAMessage() { harness.server.on("dev.hatk.createRecord", """{"error":"no"}""", code = 500) val model = viewModel() model.state.awaitValue("the profile") { it.profile != null }
model.toggleBlock()
val state = model.state.awaitValue("the revert") { it.errorMessage != null } assertFalse(state.isBlocking) assertEquals("Couldn't block that account. Try again.", state.errorMessage) assertFalse(state.isModerationPending) }
@Test fun mutingFlipsTheStateAndTellsTheServer() { val model = viewModel() model.state.awaitValue("the profile") { it.profile != null }
model.toggleMute()
assertTrue(model.state.value.isMuted) assertTrue( harness.server.awaitCall("social.grain.graph.muteActor").body.contains(alice), ) model.state.awaitValue("the write to settle") { !it.isModerationPending } }
@Test fun unmutingCallsTheOtherEndpoint() { harness.server.on( "social.grain.unspecced.getActorProfile", json(Fixtures.profileDetailed(muted = true)), ) val model = viewModel() model.state.awaitValue("the profile") { it.isMuted }
model.toggleMute()
harness.server.awaitCall("social.grain.graph.unmuteActor") assertFalse(model.state.value.isMuted) }
@Test fun aMuteThatFailsIsPutBackWithAMessage() { harness.server.on("social.grain.graph.muteActor", """{"error":"no"}""", code = 500) val model = viewModel() model.state.awaitValue("the profile") { it.profile != null }
model.toggleMute()
val state = model.state.awaitValue("the revert") { it.errorMessage != null } assertFalse(state.isMuted) assertEquals("Couldn't mute that account. Try again.", state.errorMessage) }
@Test fun deletingAGalleryRemovesItFromTheGrid() { val model = viewModel() val gallery = model.state.awaitValue("the grid") { it.galleries.isNotEmpty() } .galleries.single()
model.deleteGallery(gallery.uri)
model.state.awaitValue("the tile to go") { it.galleries.isEmpty() } }
@Test fun aGalleryDeleteThatFailsSaysSo() { harness.server.on( "social.grain.unspecced.deleteGallery", """{"error":"no"}""", code = 500, ) val model = viewModel() val gallery = model.state.awaitValue("the grid") { it.galleries.isNotEmpty() } .galleries.single()
model.deleteGallery(gallery.uri)
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals(1, state.galleries.size) }
@Test fun editingThisProfileElsewhereMakesItReRead() { val model = viewModel() model.state.awaitValue("the profile") { it.profile != null } val before = harness.server.countOf("social.grain.unspecced.getActorProfile")
invalidation.invalidate("did:plc:someone-else") settle() assertEquals(before, harness.server.countOf("social.grain.unspecced.getActorProfile"))
invalidation.invalidate(alice) harness.server.awaitCall("social.grain.unspecced.getActorProfile", before + 1) }}