package social.grain import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.runners.AndroidJUnit4 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.data.local.ProfileInvalidation import social.grain.feature.profile.ProfileFeedSource import social.grain.feature.profile.ProfileGalleryFeedScreen import social.grain.feature.profile.ProfileTab import social.grain.feature.profile.ProfileViewModel import social.grain.support.Fixtures import social.grain.support.Harness import social.grain.support.MainDispatcherRule import social.grain.support.awaitText import social.grain.support.awaitValue import social.grain.support.json import social.grain.ui.theme.GrainTheme /** * The profile's grid unrolled into cards. * * What matters is where it opens: the tile that was tapped should be the first * card on screen, with nothing above it composed, rather than the top of the * list with a scroll still to come. */ @RunWith(AndroidJUnit4::class) class ProfileGalleryFeedScreenTest { @get:Rule val mainDispatcher = MainDispatcherRule() @get:Rule val composeRule = createComposeRule() private lateinit var harness: Harness private val titles = listOf("First", "Second", "Third", "Fourth", "Fifth") private fun feed(vararg titles: String) = titles .mapIndexed { i, title -> json(Fixtures.gallery(rkey = "g$i", title = title)) } .joinToString(",", prefix = """{"items":[""", postfix = "]}") @Before fun setUp() { harness = Harness() harness.server .on("social.grain.actor.getProfile", json(Fixtures.profileDetailed())) .on("social.grain.feed.getFeed", feed(*titles.toTypedArray())) .on("social.grain.story.getStories", """{"stories":[]}""") .on("social.grain.actor.getFavorites", feed("Faved")) .on("social.grain.story.getArchive", """{"stories":[]}""") } @After fun tearDown() = harness.tearDown() private fun viewModel(did: String? = "did:plc:alice") = ProfileViewModel( client = harness.client, auth = harness.auth, profileInvalidation = ProfileInvalidation(), labelDefsCache = harness.labelDefinitions, savedStateHandle = SavedStateHandle( if (did == null) emptyMap() else mapOf("did" to did), ), ) private fun show( model: ProfileViewModel, uri: String, source: ProfileFeedSource = ProfileFeedSource.GALLERIES, onBack: () -> Unit = {}, ) { composeRule.setContent { GrainTheme { ProfileGalleryFeedScreen( uri = uri, source = source, onBack = onBack, onOpenProfile = {}, onOpenHashtag = {}, viewModel = model, ) } } composeRule.waitForIdle() } @Test fun theFeedOpensOnTheTappedGalleryNotTheTopOfTheList() { val model = viewModel() val galleries = model.state.awaitValue("the grid") { it.galleries.size == titles.size } .galleries show(model, uri = galleries[3].uri) composeRule.awaitText("Fourth") composeRule.onNodeWithText("Fourth").assertIsDisplayed() // Above the fold means not composed at all in a lazy list — the reader // landed on the fourth card, not scrolled past the first three. composeRule.onAllNodesWithText("First").assertCountEquals(0) } @Test fun theFeedIsTitledWithTheProfilesHandleAndGoesBack() { var back = 0 val model = viewModel() val first = model.state.awaitValue("the grid") { it.galleries.isNotEmpty() } .galleries.first() show(model, uri = first.uri, onBack = { back += 1 }) composeRule.awaitText("alice.grain.social") composeRule.awaitText("First") composeRule.onNodeWithContentDescription("Back").performClick() assertEquals(1, back) } @Test fun aFeedFromTheFavoritesTabListsTheFavorites() { harness.server.on( "social.grain.actor.getProfile", json(Fixtures.profileDetailed(did = harness.did, handle = harness.handle)), ) val model = viewModel(did = null) model.state.awaitValue("the profile") { it.profile != null } model.selectTab(ProfileTab.FAVORITES) val faved = model.state.awaitValue("the favorites") { it.favoritesLoaded } .favorites.single() show(model, uri = faved.uri, source = ProfileFeedSource.FAVORITES) composeRule.awaitText("Favorites") composeRule.awaitText("Faved") composeRule.onAllNodesWithText("First").assertCountEquals(0) } @Test fun aTappedGalleryThatIsNoLongerInTheListOpensAtTheTop() { val model = viewModel() model.state.awaitValue("the grid") { it.galleries.isNotEmpty() } show(model, uri = "at://did:plc:alice/social.grain.gallery/gone") composeRule.awaitText("First") } }