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

grain.social/grain-android

1 collaborator
0
1
main
219 lines · 7.1 KB Raw History
package social.grain

import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
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.feature.notifications.NotificationsScreen
import social.grain.feature.notifications.NotificationsViewModel
import social.grain.feature.notifications.UnseenNotificationCount
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.appContext
import social.grain.support.json

/**
 * The notifications list as a reader sees it.
 *
 * Grouping is the whole point of the screen — "Alice and 2 others favorited
 * your gallery" is one row — and the sentence it builds is assembled in the
 * composable, so nothing below it can catch a wrong one.
 */
@RunWith(AndroidJUnit4::class)
class NotificationsScreenTest {

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    @get:Rule
    val composeRule = createComposeRule()

    private lateinit var harness: Harness

    private val nsid = "social.grain.notification.listNotifications"

    private fun page(vararg items: String) =
        """{"notifications":[${items.joinToString(",")}]}"""

    @Before
    fun setUp() {
        harness = Harness()
    }

    @After
    fun tearDown() = harness.tearDown()

    private fun show(
        onOpenGallery: (String) -> Unit = {},
        onOpenProfile: (String) -> Unit = {},
    ) {
        val model = NotificationsViewModel(
            harness.client,
            harness.auth,
            UnseenNotificationCount(appContext, harness.client, harness.auth),
        )
        composeRule.setContent {
            social.grain.ui.theme.GrainTheme {
                NotificationsScreen(
                    onOpenGallery = onOpenGallery,
                    onOpenProfile = onOpenProfile,
                    viewModel = model,
                )
            }
        }
        composeRule.waitForIdle()
    }

    @Test
    fun anAccountWithNothingWaitingIsToldSo() {
        harness.server.on(nsid, page())
        show()

        composeRule.awaitText("Notifications")
        composeRule.awaitText("Nothing yet")
        composeRule.onNodeWithText("Favorites, comments and follows will show up here.")
            .assertExists()
    }

    @Test
    fun oneFavoriteReadsAsOnePersonFavoritingOneGallery() {
        harness.server.on(nsid, page(json(Fixtures.notification())))
        show()

        composeRule.awaitText("Alice favorited your gallery", substring = true)
        composeRule.awaitText("A walk in the fog")
    }

    @Test
    fun severalFavoritesOnOneGalleryCollapseIntoOneSentence() {
        harness.server.on(
            nsid,
            page(
                json(Fixtures.notification(rkey = "n1")),
                json(
                    Fixtures.notification(
                        rkey = "n2",
                        author = Fixtures.profile(did = "did:plc:bob", displayName = "Bob"),
                    ),
                ),
                json(
                    Fixtures.notification(
                        rkey = "n3",
                        author = Fixtures.profile(did = "did:plc:carol", displayName = "Carol"),
                    ),
                ),
            ),
        )
        show()

        composeRule.awaitText("Alice and 2 others favorited your gallery", substring = true)
    }

    @Test
    fun twoPeopleReadAsOneOther() {
        harness.server.on(
            nsid,
            page(
                json(Fixtures.notification(rkey = "n1")),
                json(
                    Fixtures.notification(
                        rkey = "n2",
                        author = Fixtures.profile(did = "did:plc:bob", displayName = "Bob"),
                    ),
                ),
            ),
        )
        show()

        composeRule.awaitText("Alice and 1 other favorited your gallery", substring = true)
    }

    /** Three at a time: a lazy list only composes what fits on screen. */
    @Test
    fun followsCommentsAndRepliesEachReadDifferently() {
        harness.server.on(
            nsid,
            page(
                json(Fixtures.notification(rkey = "n1", reason = "follow", galleryUri = null)),
                json(
                    Fixtures.notification(
                        rkey = "n2",
                        reason = "gallery-comment",
                        commentText = "Lovely light",
                    ),
                ),
                json(Fixtures.notification(rkey = "n3", reason = "reply")),
            ),
        )
        show()

        composeRule.awaitText("followed you", substring = true)
        composeRule.awaitText("commented on your gallery", substring = true)
        composeRule.awaitText("Lovely light")
        composeRule.awaitText("replied to your comment", substring = true)
    }

    @Test
    fun mentionsStoriesAndAnUnknownReasonAreAllHandled() {
        harness.server.on(
            nsid,
            page(
                json(Fixtures.notification(rkey = "n4", reason = "gallery-mention")),
                json(Fixtures.notification(rkey = "n5", reason = "story-favorite")),
                json(Fixtures.notification(rkey = "n6", reason = "who-knows")),
            ),
        )
        show()

        composeRule.awaitText("mentioned you in a gallery", substring = true)
        composeRule.awaitText("favorited your story", substring = true)
        composeRule.onNodeWithText("A reason this build has never heard of says nothing rather than crashing", substring = true)
            .assertDoesNotExist()
    }

    @Test
    fun tappingARowOpensTheGalleryItIsAbout() {
        harness.server.on(nsid, page(json(Fixtures.notification())))
        var opened: String? = null
        show(onOpenGallery = { opened = it })

        composeRule.onNodeWithText("Alice favorited your gallery", substring = true).performClick()

        assertEquals("at://did:plc:alice/social.grain.gallery/gallery1", opened)
    }

    @Test
    fun aFollowHasNoGalleryToOpenSoItOpensTheProfileInstead() {
        harness.server.on(
            nsid,
            page(json(Fixtures.notification(reason = "follow", galleryUri = null))),
        )
        var profile: String? = null
        show(onOpenProfile = { profile = it })

        composeRule.onNodeWithText("Alice followed you", substring = true).performClick()

        assertEquals("did:plc:alice", profile)
    }

    /** Arriving clears the badge, without waiting for a scroll to the bottom. */
    @Test
    fun openingTheScreenIsTheReadReceipt() {
        harness.server.on(nsid, page(json(Fixtures.notification())))
        harness.server.on("social.grain.notification.getUnreadCount", """{"count":1}""")
        show()

        composeRule.waitUntil(10_000) {
            harness.server.countOf("social.grain.notification.updateSeen") == 1
        }
    }
}