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.unspecced.getNotifications" private fun page(vararg items: String) = """{"notifications":[${items.joinToString(",")}],"unseenCount":1}""" @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()))) show() composeRule.waitUntil(10_000) { harness.server.countOf("dev.hatk.putPreference") == 1 } } }