~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
242 lines · 7.8 KB Raw History
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<CommentThread> = 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<String, GrainComment?>? = 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<String, GrainComment?>? = 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)
    }
}