Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368package social.grain
import androidx.compose.ui.test.junit4.v2.createComposeRuleimport androidx.compose.ui.test.onNodeWithContentDescriptionimport androidx.compose.ui.test.onNodeWithTextimport androidx.compose.ui.test.performClickimport androidx.lifecycle.SavedStateHandleimport androidx.test.ext.junit.runners.AndroidJUnit4import okhttp3.OkHttpClientimport org.junit.Afterimport org.junit.Assert.assertEqualsimport org.junit.Beforeimport org.junit.Ruleimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.data.location.LocationServicesimport social.grain.feature.actors.ActorListModeimport social.grain.feature.actors.ActorListScreenimport social.grain.feature.actors.ActorListViewModelimport social.grain.feature.feed.HashtagFeedScreenimport social.grain.feature.feed.HashtagFeedViewModelimport social.grain.feature.feed.LocationFeedScreenimport social.grain.feature.feed.LocationFeedViewModelimport social.grain.feature.gallery.GalleryDetailScreenimport social.grain.feature.gallery.GalleryDetailViewModelimport social.grain.feature.settings.ModerationModeimport social.grain.feature.settings.ModerationScreenimport social.grain.feature.settings.ModerationViewModelimport social.grain.support.Fixturesimport social.grain.support.Harnessimport social.grain.support.awaitContentDescriptionimport social.grain.support.awaitTextimport social.grain.support.MainDispatcherRuleimport social.grain.support.awaitCallimport social.grain.support.jsonimport social.grain.support.testJsonimport social.grain.ui.theme.GrainTheme
/** * The screens that are a list, a title bar and an empty state. * * Grouped because that is genuinely all they are: each wraps one view model * already tested on its own, and what is worth pinning here is the shell — the * right title, the right words when there is nothing to show, and a back button * that reports its tap. */@RunWith(AndroidJUnit4::class)class ScreenShellTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
@get:Rule val composeRule = createComposeRule()
private lateinit var harness: Harness
@Before fun setUp() { harness = Harness() harness.server .on("dev.hatk.describeLabels", """{"definitions":[]}""") .on("dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery())}]}""") }
@After fun tearDown() = harness.tearDown()
// MARK: - Actor lists
private fun actorList( mode: ActorListMode, subject: String = "did:plc:alice", onBack: () -> Unit = {}, onOpenProfile: (String) -> Unit = {}, ) { val model = ActorListViewModel( harness.client, harness.auth, SavedStateHandle(mapOf("mode" to mode, "subject" to subject)), ) composeRule.setContent { GrainTheme { ActorListScreen(onBack = onBack, onOpenProfile = onOpenProfile, viewModel = model) } } composeRule.waitForIdle() }
@Test fun aFollowerListIsTitledAndListsWhoItWasGiven() { harness.server.on( "social.grain.unspecced.getFollowers", """{"totalCount":1,"items":[{"did":"did:plc:bob","handle":"bob.test","displayName":"Bob"}]}""", ) var opened: String? = null actorList(ActorListMode.FOLLOWERS, onOpenProfile = { opened = it })
composeRule.awaitText("Followers") composeRule.awaitText("Bob")
composeRule.onNodeWithText("Bob").performClick() assertEquals("did:plc:bob", opened) }
@Test fun anEmptyFollowerListSaysWhoItIsAboutRatherThanNothing() { harness.server.on("social.grain.unspecced.getFollowers", """{"items":[]}""") actorList(ActorListMode.FOLLOWERS)
composeRule.awaitText("No followers") composeRule.awaitText("No one is following this account yet.") }
@Test fun anEmptyFavoritesListHasItsOwnWording() { harness.server.on("social.grain.unspecced.getGalleryFavorites", """{"items":[]}""") actorList(ActorListMode.GALLERY_FAVORITES, subject = "at://x/gallery/1")
composeRule.awaitText("Favorited by") composeRule.awaitText("No one has favorited this gallery yet.") }
@Test fun theActorListBackButtonReportsItsTap() { harness.server.on("social.grain.unspecced.getFollowing", """{"items":[]}""") var back = 0 actorList(ActorListMode.FOLLOWING, onBack = { back += 1 })
composeRule.onNodeWithContentDescription("Back").performClick()
assertEquals(1, back) }
// MARK: - Moderation lists
private fun moderation(mode: ModerationMode, onBack: () -> Unit = {}) { val model = ModerationViewModel( harness.client, harness.auth, SavedStateHandle(mapOf("mode" to mode)), ) composeRule.setContent { GrainTheme { ModerationScreen(onBack = onBack, onOpenProfile = {}, viewModel = model) } } composeRule.waitForIdle() }
@Test fun theBlockedListNamesWhoIsBlockedAndOffersToUndoIt() { harness.server.on( "social.grain.unspecced.getBlocks", """{"items":[{"did":"did:plc:bob","handle":"bob.test","displayName":"Bob",""" + """"blockUri":"at://did:plc:me/social.grain.graph.block/b1"}]}""", ) moderation(ModerationMode.BLOCKED)
composeRule.awaitText("Blocked users") composeRule.awaitText("Bob") composeRule.onNodeWithText("Unblock").performClick()
composeRule.waitUntil(10_000) { harness.server.countOf("dev.hatk.deleteRecord") >= 1 } }
@Test fun anEmptyMutedListSaysSo() { harness.server.on("social.grain.unspecced.getMutes", """{"items":[]}""") var back = 0 moderation(ModerationMode.MUTED, onBack = { back += 1 })
composeRule.awaitText("Muted users") composeRule.awaitText("You haven't muted anyone.")
composeRule.onNodeWithContentDescription("Back").performClick() assertEquals(1, back) }
// MARK: - Gallery detail
private fun galleryDetail(onBack: () -> Unit = {}) { val model = GalleryDetailViewModel( harness.client, harness.auth, harness.labelDefinitions, SavedStateHandle(mapOf("uri" to "at://did:plc:alice/social.grain.gallery/gallery1")), ) composeRule.setContent { GrainTheme { GalleryDetailScreen(onBack = onBack, onOpenProfile = {}, viewModel = model) } } composeRule.waitForIdle() }
@Test fun theGalleryScreenDrawsTheCardAndItsCommentPrompt() { harness.server .on( "social.grain.unspecced.getGallery", """{"gallery":${json(Fixtures.gallery())}}""", ) .on( "social.grain.unspecced.getCommentThread", """{"comments":[${json(Fixtures.comment())}],"totalCount":3}""", ) galleryDetail()
composeRule.awaitText("Gallery") composeRule.awaitText("A walk in the fog") composeRule.awaitText("View all 3 comments") }
@Test fun aGalleryWithNoCommentsInvitesTheFirstOne() { harness.server .on( "social.grain.unspecced.getGallery", """{"gallery":${json(Fixtures.gallery(commentCount = 0))}}""", ) .on("social.grain.unspecced.getCommentThread", """{"comments":[],"totalCount":0}""") galleryDetail()
composeRule.awaitText("Add a comment…") }
@Test fun aGalleryThatIsGoneSaysSoRatherThanSpinningForever() { harness.server .on("social.grain.unspecced.getGallery", """{"error":"gone"}""", code = 404) .on("social.grain.unspecced.getCommentThread", """{"comments":[]}""") var back = 0 galleryDetail(onBack = { back += 1 })
composeRule.awaitText("Gallery unavailable")
composeRule.onNodeWithContentDescription("Back").performClick() assertEquals(1, back) }
@Test fun theViewersOwnGalleryOffersToDeleteIt() { harness.server .on( "social.grain.unspecced.getGallery", """{"gallery":${json( Fixtures.gallery( creator = Fixtures.profile(did = "did:plc:me", handle = "me.test"), ), )}}""", ) .on("social.grain.unspecced.getCommentThread", """{"comments":[]}""") galleryDetail()
composeRule.onNodeWithContentDescription("Delete gallery").performClick() composeRule.awaitText("Delete gallery?") composeRule.onNodeWithText("Cancel").performClick() composeRule.waitForIdle()
assertEquals(0, harness.server.countOf("social.grain.unspecced.deleteGallery")) }
// MARK: - Tapped-through feeds
@Test fun aHashtagFeedIsTitledAfterItsTagAndListsWhatItFound() { var back = 0 val model = HashtagFeedViewModel( harness.client, harness.auth, harness.labelDefinitions, SavedStateHandle(mapOf("tag" to "fog")), ) composeRule.setContent { GrainTheme { HashtagFeedScreen( onBack = { back += 1 }, onOpenProfile = {}, onOpenHashtag = {}, viewModel = model, ) } } composeRule.waitForIdle()
composeRule.awaitText("#fog") composeRule.awaitText("A walk in the fog")
composeRule.onNodeWithContentDescription("Back").performClick() assertEquals(1, back) }
@Test fun anEmptyHashtagFeedNamesTheTagThatFoundNothing() { harness.server.on("dev.hatk.getFeed", """{"items":[]}""") val model = HashtagFeedViewModel( harness.client, harness.auth, harness.labelDefinitions, SavedStateHandle(mapOf("tag" to "quiet")), ) composeRule.setContent { GrainTheme { HashtagFeedScreen( onBack = {}, onOpenProfile = {}, onOpenHashtag = {}, viewModel = model, ) } } composeRule.waitForIdle()
composeRule.awaitText("Nothing tagged #quiet yet") }
@Test fun aLocationFeedIsTitledWithThePlaceNameTheReaderTapped() { val model = LocationFeedViewModel( harness.client, harness.auth, LocationServices(OkHttpClient(), testJson), harness.labelDefinitions, SavedStateHandle(mapOf("cell" to "8928308280fffff", "name" to "Mission District")), ) composeRule.setContent { GrainTheme { LocationFeedScreen( onBack = {}, onOpenProfile = {}, onOpenHashtag = {}, viewModel = model, ) } } composeRule.waitForIdle()
composeRule.awaitText("Mission District") composeRule.awaitText("A walk in the fog") }
@Test fun anEmptyLocationFeedNamesThePlaceThatFoundNothing() { harness.server.on("dev.hatk.getFeed", """{"items":[]}""") val model = LocationFeedViewModel( harness.client, harness.auth, LocationServices(OkHttpClient(), testJson), harness.labelDefinitions, SavedStateHandle(mapOf("cell" to "89283", "name" to "Nowhere")), ) composeRule.setContent { GrainTheme { LocationFeedScreen( onBack = {}, onOpenProfile = {}, onOpenHashtag = {}, viewModel = model, ) } } composeRule.waitForIdle()
composeRule.awaitText("Nothing from Nowhere yet") }}