Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298package social.grain
import androidx.test.ext.junit.runners.AndroidJUnit4import org.junit.Afterimport org.junit.Assert.assertEqualsimport org.junit.Assert.assertFalseimport org.junit.Assert.assertNullimport org.junit.Assert.assertTrueimport org.junit.Beforeimport org.junit.Ruleimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.feature.gallery.GalleryCommentsViewModelimport social.grain.support.Fixturesimport social.grain.support.Harnessimport social.grain.support.MainDispatcherRuleimport social.grain.support.awaitCallimport social.grain.support.awaitValueimport social.grain.support.jsonimport social.grain.support.settleimport java.util.concurrent.CountDownLatchimport java.util.concurrent.TimeUnit
/** * The comment sheet opened over a feed card. * * One instance is reused as the reader opens the sheet on one card after * another, so the subject changing underneath an in-flight request is the * normal case here rather than an edge one — a late reply landing under the * wrong gallery is the failure this class is shaped to avoid. */@RunWith(AndroidJUnit4::class)class GalleryCommentsViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private lateinit var harness: Harness
private val first = "at://did:plc:alice/social.grain.gallery/one" private val second = "at://did:plc:alice/social.grain.gallery/two"
private val nsid = "social.grain.unspecced.getCommentThread"
private fun thread(vararg comments: String, total: Int? = null, cursor: String? = null) = """{"comments":[${comments.joinToString(",")}]""" + (total?.let { ""","totalCount":$it""" } ?: "") + (cursor?.let { ""","cursor":"$it"""" } ?: "") + "}"
@Before fun setUp() { harness = Harness() harness.server.on(nsid, thread(json(Fixtures.comment()), total = 3)) }
@After fun tearDown() = harness.tearDown()
private fun viewModel() = GalleryCommentsViewModel(harness.client, harness.auth)
@Test fun openingASheetReadsThatGallerysThread() { val model = viewModel() model.open(first)
val state = model.state.awaitValue("the thread") { it.hasLoaded } assertEquals(first, state.galleryUri) assertEquals(1, state.comments.size) assertEquals(3, state.totalCount) assertEquals(harness.did, state.viewerDid) }
@Test fun aThreadWithNoTotalCountFallsBackToWhatItActuallyGotBack() { harness.server.on(nsid, thread(json(Fixtures.comment()), json(Fixtures.comment(rkey = "c2")))) val model = viewModel() model.open(first)
assertEquals(2, model.state.awaitValue("the thread") { it.hasLoaded }.totalCount) }
@Test fun openingADifferentGalleryClearsWhatTheLastOneShowed() { val model = viewModel() model.open(first) model.state.awaitValue("the first thread") { it.hasLoaded }
// Held open so the clearing can be observed before the answer lands — // the point of it is what the reader sees in between. val answer = CountDownLatch(1) harness.server.on(nsid) { answer.await(5, TimeUnit.SECONDS) thread(json(Fixtures.comment(rkey = "other", text = "Elsewhere"))) } model.open(second)
assertTrue("The previous gallery's rows go immediately", model.state.value.comments.isEmpty()) assertFalse(model.state.value.hasLoaded)
answer.countDown() val state = model.state.awaitValue("the second thread") { it.hasLoaded } assertEquals("Elsewhere", state.comments.single().text) }
@Test fun reopeningTheSameGalleryKeepsTheRowsWhileItRefetches() { val model = viewModel() model.open(first) model.state.awaitValue("the thread") { it.hasLoaded }
model.open(first)
assertTrue("No spinner over rows we already have", model.state.value.hasLoaded) assertEquals(1, model.state.value.comments.size) }
@Test fun aThreadThatFailsToLoadSaysSo() { harness.server.on(nsid, """{"error":"nope"}""", code = 500) val model = viewModel() model.open(first)
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertTrue(state.hasLoaded) assertEquals("Couldn't load comments.", state.errorMessage)
model.dismissError() assertNull(model.state.value.errorMessage) }
@Test fun moreCommentsAppendBehindTheCursor() { harness.server.on(nsid, thread(json(Fixtures.comment()), cursor = "c1")) val model = viewModel() model.open(first) model.state.awaitValue("the thread") { it.hasLoaded }
harness.server.on(nsid, thread(json(Fixtures.comment(rkey = "c2", text = "Second")))) model.loadMore()
val state = model.state.awaitValue("the second page") { it.comments.size == 2 } assertEquals("Second", state.comments[1].text) assertFalse(state.isLoadingMore) }
@Test fun loadingMoreBeforeASheetIsOpenDoesNothing() { viewModel().loadMore() settle() assertEquals(0, harness.server.countOf(nsid)) }
@Test fun postingACommentReReadsTheThread() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/comment/new"}""") val model = viewModel() model.open(first) model.state.awaitValue("the thread") { it.hasLoaded }
harness.server.on( nsid, thread(json(Fixtures.comment()), json(Fixtures.comment(rkey = "c2", text = "Mine")), total = 4), ) model.postComment(" Mine ", replyTo = null)
val state = model.state.awaitValue("the reread thread") { it.comments.size == 2 } assertEquals(4, state.totalCount) assertFalse(state.isPosting) assertTrue(harness.server.awaitCall("dev.hatk.createRecord").body.contains("\"text\":\"Mine\"")) }
@Test fun anEmptyCommentIsNotPosted() { val model = viewModel() model.open(first) model.state.awaitValue("the thread") { it.hasLoaded }
model.postComment(" ", replyTo = null) settle()
assertEquals(0, harness.server.countOf("dev.hatk.createRecord")) }
@Test fun postingWithNoSheetOpenDoesNothing() { viewModel().postComment("Hello", replyTo = null) settle() assertEquals(0, harness.server.countOf("dev.hatk.createRecord")) }
@Test fun aCommentThatFailsToPostSaysSo() { harness.server.on("dev.hatk.createRecord", """{"error":"no"}""", code = 500) val model = viewModel() model.open(first) model.state.awaitValue("the thread") { it.hasLoaded }
model.postComment("Mine", replyTo = null)
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals("Couldn't post that comment.", state.errorMessage) assertFalse(state.isPosting) }
@Test fun deletingACommentTakesItsRepliesAndTheCountWithIt() { val root = Fixtures.comment(rkey = "root") harness.server.on( nsid, thread( json(root), json(Fixtures.comment(rkey = "reply", replyTo = root.uri)), total = 2, ), ) val model = viewModel() model.open(first) val loaded = model.state.awaitValue("the thread") { it.comments.size == 2 }
model.deleteComment(loaded.comments.first())
val state = model.state.awaitValue("the rows to go") { it.comments.isEmpty() } assertEquals(0, state.totalCount) }
@Test fun aDeleteThatFailsPutsTheRowsBack() { harness.server.on("dev.hatk.deleteRecord", """{"error":"no"}""", code = 500) val model = viewModel() model.open(first) val loaded = model.state.awaitValue("the thread") { it.hasLoaded }
model.deleteComment(loaded.comments.single())
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals(1, state.comments.size) assertEquals(3, state.totalCount) }
@Test fun favoritingACommentFlipsItThenKeepsTheRecordUri() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/fav/1"}""") val model = viewModel() model.open(first) val comment = model.state.awaitValue("the thread") { it.hasLoaded }.comments.single()
model.toggleFavorite(comment)
assertEquals(1, model.state.value.comments.single().favCount) model.state.awaitValue("the record uri") { it.comments.single().viewer?.fav == "at://did:plc:me/fav/1" } }
@Test fun aCommentFavoriteThatFailsIsPutBack() { harness.server.on("dev.hatk.createRecord", """{"error":"no"}""", code = 500) val model = viewModel() model.open(first) val comment = model.state.awaitValue("the thread") { it.hasLoaded }.comments.single()
model.toggleFavorite(comment)
val state = model.state.awaitValue("the revert") { it.comments.single().viewer?.fav == null } assertEquals(0, state.comments.single().favCount) }
@Test fun unfavoritingACommentDeletesTheRecord() { harness.server.on( nsid, thread(json(Fixtures.comment(favUri = "at://did:plc:me/social.grain.favorite/cf1"))), ) val model = viewModel() model.open(first) val comment = model.state.awaitValue("the thread") { it.hasLoaded }.comments.single()
model.toggleFavorite(comment)
assertTrue(harness.server.awaitCall("dev.hatk.deleteRecord").body.contains("cf1")) }
@Test fun repliesAreGroupedUnderTheirRoot() { val root = Fixtures.comment(rkey = "root", text = "Root") harness.server.on( nsid, thread(json(root), json(Fixtures.comment(rkey = "r", text = "Reply", replyTo = root.uri))), ) val model = viewModel() model.open(first) val state = model.state.awaitValue("the thread") { it.comments.size == 2 }
assertEquals(1, state.threads.size) assertEquals("Reply", state.threads.single().replies.single().text) }}