package social.grain import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import social.grain.data.model.GrainComment import social.grain.data.model.GrainProfile import social.grain.feature.gallery.GalleryDetailUiState /** * The comment thread arrives flat, with replies naming their root. Grouping it * wrong either drops replies off the end of the list or nests them under the * wrong parent, and both look like data loss to the reader. */ class CommentThreadingTest { private fun comment(uri: String, replyTo: String? = null, handle: String = "a.test") = GrainComment( uri = uri, author = GrainProfile(did = "did:plc:$handle", handle = handle), text = "text for $uri", replyTo = replyTo, ) private fun stateOf(vararg comments: GrainComment) = GalleryDetailUiState(comments = comments.toList()) @Test fun `roots keep their order and carry their own replies`() { val state = stateOf( comment("at://root1"), comment("at://root2"), comment("at://reply1", replyTo = "at://root1"), comment("at://reply2", replyTo = "at://root2"), comment("at://reply3", replyTo = "at://root1"), ) val threads = state.threads assertEquals(listOf("at://root1", "at://root2"), threads.map { it.root.uri }) assertEquals(listOf("at://reply1", "at://reply3"), threads[0].replies.map { it.uri }) assertEquals(listOf("at://reply2"), threads[1].replies.map { it.uri }) } @Test fun `every comment appears exactly once`() { val all = listOf( comment("at://a"), comment("at://b", replyTo = "at://a"), comment("at://c"), comment("at://d", replyTo = "at://c"), comment("at://e", replyTo = "at://a"), ) val threads = stateOf(*all.toTypedArray()).threads val rendered = threads.flatMap { listOf(it.root.uri) + it.replies.map { r -> r.uri } } assertEquals(all.size, rendered.size) assertEquals(all.map { it.uri }.toSet(), rendered.toSet()) } @Test fun `a reply whose root is missing from the page is not silently dropped twice`() { // The root may sit on a later page. Such a reply has no thread to join, // so it must not be counted as a root either — this pins the current // behaviour so a future change to it is deliberate. val threads = stateOf( comment("at://root"), comment("at://orphan", replyTo = "at://not-on-this-page"), ).threads assertEquals(listOf("at://root"), threads.map { it.root.uri }) assertTrue("An orphaned reply must not be promoted to a root", threads.all { it.replies.isEmpty() }) } @Test fun `a thread with no replies still renders its root`() { val threads = stateOf(comment("at://solo")).threads assertEquals(1, threads.size) assertTrue(threads[0].replies.isEmpty()) } @Test fun `no comments means no threads`() { assertTrue(stateOf().threads.isEmpty()) } }