package social.grain import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import social.grain.data.model.GrainNotification import social.grain.data.model.GrainProfile import social.grain.data.model.GroupedNotification import java.time.Instant /** * Grouping is what turns a wall of "X favorited your gallery" into one row. * Getting it wrong either buries real events or merges things that aren't the * same event, and neither is visible from the code alone. */ class NotificationGroupingTest { private val base = Instant.parse("2026-08-21T12:00:00.000Z") private fun notif( uri: String, reason: String, author: String, gallery: String? = null, hoursAgo: Long = 0, ) = GrainNotification( uri = uri, reason = reason, createdAt = base.minusSeconds(hoursAgo * 3600).toString(), author = GrainProfile(did = "did:plc:$author", handle = "$author.test"), galleryUri = gallery, ) @Test fun `favorites on the same gallery collapse into one row`() { val groups = GroupedNotification.group( listOf( notif("at://1", "gallery-favorite", "alice", gallery = "at://g/1"), notif("at://2", "gallery-favorite", "bob", gallery = "at://g/1", hoursAgo = 1), notif("at://3", "gallery-favorite", "carol", gallery = "at://g/1", hoursAgo = 2), ), ) assertEquals(1, groups.size) assertEquals(3, groups.single().authorCount) assertTrue(groups.single().isGrouped) } @Test fun `favorites on different galleries stay separate`() { val groups = GroupedNotification.group( listOf( notif("at://1", "gallery-favorite", "alice", gallery = "at://g/1"), notif("at://2", "gallery-favorite", "bob", gallery = "at://g/2"), ), ) assertEquals(2, groups.size) } @Test fun `the same person favoriting twice is counted once`() { val groups = GroupedNotification.group( listOf( notif("at://1", "gallery-favorite", "alice", gallery = "at://g/1"), notif("at://2", "gallery-favorite", "alice", gallery = "at://g/1", hoursAgo = 1), ), ) assertEquals(1, groups.size) assertEquals(1, groups.single().authorCount) } @Test fun `events more than two days apart are separate rows`() { val groups = GroupedNotification.group( listOf( notif("at://1", "gallery-favorite", "alice", gallery = "at://g/1"), notif("at://2", "gallery-favorite", "bob", gallery = "at://g/1", hoursAgo = 72), ), ) assertEquals("a favorite three days later is its own event", 2, groups.size) } @Test fun `follows group across subjects because they have none`() { val groups = GroupedNotification.group( listOf( notif("at://1", "follow", "alice"), notif("at://2", "follow", "bob", hoursAgo = 1), ), ) assertEquals(1, groups.size) assertEquals(2, groups.single().authorCount) } @Test fun `comments never group even on the same gallery`() { // Each comment carries its own text; collapsing them would hide it. val groups = GroupedNotification.group( listOf( notif("at://1", "gallery-comment", "alice", gallery = "at://g/1"), notif("at://2", "gallery-comment", "bob", gallery = "at://g/1"), ), ) assertEquals(2, groups.size) assertTrue(groups.all { !it.isGrouped }) } @Test fun `a favorite and a comment on one gallery do not merge`() { val groups = GroupedNotification.group( listOf( notif("at://1", "gallery-favorite", "alice", gallery = "at://g/1"), notif("at://2", "gallery-comment", "bob", gallery = "at://g/1"), ), ) assertEquals(2, groups.size) } @Test fun `merging a page extends existing groups rather than reordering`() { val groups = GroupedNotification.group( listOf(notif("at://1", "gallery-favorite", "alice", gallery = "at://g/1")), ).toMutableList() GroupedNotification.merge( listOf(notif("at://2", "gallery-favorite", "bob", gallery = "at://g/1", hoursAgo = 1)), groups, ) assertEquals(1, groups.size) assertEquals(2, groups.single().authorCount) assertEquals("the original stays the group's head", "at://1", groups.single().notification.uri) } @Test fun `an unknown reason still renders as its own row`() { val groups = GroupedNotification.group( listOf(notif("at://1", "something-new-from-the-server", "alice")), ) assertEquals(1, groups.size) assertFalse(groups.single().isGrouped) } }