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.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import social.grain.data.model.CommentThread import social.grain.data.model.GrainComment import social.grain.support.Fixtures import social.grain.support.awaitContentDescription import social.grain.support.awaitText import social.grain.ui.components.CommentSheet import social.grain.ui.theme.GrainTheme /** * The comment sheet, which is the same component over a gallery and a story. * * Everything interesting about it is composable state rather than view model * state: who the composer is replying to, whether a muted comment stays folded, * and which row actions a reader is offered on their own comment versus someone * else's. */ @RunWith(AndroidJUnit4::class) class CommentSheetTest { @get:Rule val composeRule = createComposeRule() private val me = Fixtures.profile(did = "did:plc:me", handle = "me.test", displayName = "Me") private fun show( threads: List = listOf( CommentThread(Fixtures.comment(text = "Lovely light"), emptyList()), ), viewerDid: String? = "did:plc:me", isPosting: Boolean = false, hasLoaded: Boolean = true, onDismiss: () -> Unit = {}, onPost: (String, GrainComment?) -> Unit = { _, _ -> }, onDelete: (GrainComment) -> Unit = {}, onToggleFavorite: (GrainComment) -> Unit = {}, onProfileTap: (String) -> Unit = {}, onReport: ((GrainComment) -> Unit)? = null, errorMessage: String? = null, onErrorDismissed: () -> Unit = {}, ) { composeRule.setContent { GrainTheme { CommentSheet( threads = threads, viewerDid = viewerDid, isPosting = isPosting, hasLoaded = hasLoaded, onDismiss = onDismiss, onPost = onPost, onDelete = onDelete, onToggleFavorite = onToggleFavorite, onProfileTap = onProfileTap, onLoadMore = {}, onReport = onReport, errorMessage = errorMessage, onErrorDismissed = onErrorDismissed, ) } } composeRule.waitForIdle() } @Test fun theSheetIsTitledAndShowsTheCommentsItWasGiven() { show() composeRule.awaitText("Comments") composeRule.awaitText("Lovely light") composeRule.awaitText("Alice") } @Test fun aThreadWithNothingInItSaysSoOnceItHasLoaded() { show(threads = emptyList()) composeRule.awaitText("No comments yet") } @Test fun aThreadStillLoadingSaysNothingRatherThanNoCommentsYet() { show(threads = emptyList(), hasLoaded = false) composeRule.onNodeWithText("No comments yet").assertDoesNotExist() } @Test fun repliesAreDrawnUnderTheirRoot() { val root = Fixtures.comment(rkey = "root", text = "Root") show( threads = listOf( CommentThread( root, listOf(Fixtures.comment(rkey = "r", text = "A reply", replyTo = root.uri)), ), ), ) composeRule.awaitText("Root") composeRule.awaitText("A reply") } @Test fun typingAndSendingHandsTheTextUp() { var posted: Pair? = null show(onPost = { text, replyTo -> posted = text to replyTo }) composeRule.onNodeWithText("Add a comment…").performTextInput("Nice one") composeRule.onNodeWithContentDescription("Send comment").performClick() assertEquals("Nice one", posted?.first) assertNull("A top-level comment replies to nothing", posted?.second) } @Test fun replyingNamesWhoIsBeingRepliedToAndCanBeCancelled() { show() composeRule.onNodeWithText("Reply").performClick() composeRule.awaitText("Replying to @alice.grain.social") composeRule.awaitText("Reply…") composeRule.onNodeWithText("Cancel").performClick() composeRule.onNodeWithText("Replying to @alice.grain.social").assertDoesNotExist() composeRule.awaitText("Add a comment…") } @Test fun sendingAReplyCarriesTheCommentItAnswers() { var posted: Pair? = null show(onPost = { text, replyTo -> posted = text to replyTo }) composeRule.onNodeWithText("Reply").performClick() composeRule.onNodeWithText("Reply…").performTextInput("Agreed") composeRule.onNodeWithContentDescription("Send comment").performClick() assertEquals("Agreed", posted?.first) assertEquals("comment1", posted?.second?.rkey) } @Test fun theViewersOwnCommentOffersDeleteAndNotReport() { var deleted: GrainComment? = null show( threads = listOf(CommentThread(Fixtures.comment(author = me), emptyList())), onDelete = { deleted = it }, onReport = {}, ) composeRule.onNodeWithText("Report").assertDoesNotExist() composeRule.onNodeWithText("Delete").performClick() assertEquals("comment1", deleted?.rkey) } @Test fun somebodyElsesCommentOffersReportAndNotDelete() { var reported: GrainComment? = null show(onReport = { reported = it }) composeRule.onNodeWithText("Delete").assertDoesNotExist() composeRule.onNodeWithText("Report").performClick() assertEquals("comment1", reported?.rkey) } @Test fun aSignedOutReaderIsOfferedNothingToReportWith() { show(viewerDid = null, onReport = null) composeRule.onNodeWithText("Report").assertDoesNotExist() composeRule.onNodeWithText("Delete").assertDoesNotExist() } @Test fun theHeartOnACommentSaysWhichWayItWouldGo() { var toggled: GrainComment? = null show(onToggleFavorite = { toggled = it }) composeRule.onNodeWithContentDescription("Favorite comment").performClick() assertEquals("comment1", toggled?.rkey) } @Test fun anAlreadyFavoritedCommentOffersToUndoIt() { show( threads = listOf( CommentThread(Fixtures.comment(favUri = "at://fav/1"), emptyList()), ), ) composeRule.awaitContentDescription("Unfavorite comment") } @Test fun aMutedCommentIsFoldedAwayUntilTheReaderAsksForIt() { show( threads = listOf( CommentThread( Fixtures.comment(text = "Something unpleasant").copy(muted = true), emptyList(), ), ), ) composeRule.onNodeWithText("Something unpleasant").assertDoesNotExist() composeRule.onNodeWithText("Muted comment").performClick() composeRule.awaitText("Something unpleasant") } @Test fun anErrorIsShownInsideTheSheetWhereTheReaderCanSeeIt() { var dismissed = 0 show(errorMessage = "Couldn't post that comment.", onErrorDismissed = { dismissed += 1 }) composeRule.awaitText("Couldn't post that comment.") } @Test fun tappingAnAuthorOpensTheirProfile() { var opened: String? = null show(onProfileTap = { opened = it }) composeRule.onNodeWithContentDescription("alice.grain.social's avatar").performClick() assertEquals("did:plc:alice", opened) } }