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 } } }