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

grain.social/grain-android

1 collaborator
0
1
main
144 lines · 5.0 KB Raw History
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)
    }
}