Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292package 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.stories.StoryCommentsViewModelimport 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.settle
/** * Comments on whichever story is on screen. * * The viewer swipes between stories without leaving the screen, so the badge * has to be right for every one of them without paying for a full thread fetch * per swipe — which is what the preview cache is for, and what makes "paging * back to a story already seen" the interesting case. */@RunWith(AndroidJUnit4::class)class StoryCommentsViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private lateinit var harness: Harness
private val nsid = "social.grain.unspecced.getCommentThread" private val first = "at://did:plc:alice/social.grain.story/one" private val second = "at://did:plc:alice/social.grain.story/two"
private fun thread(vararg comments: String, total: Int? = null) = """{"comments":[${comments.joinToString(",")}]""" + (total?.let { ""","totalCount":$it""" } ?: "") + "}"
@Before fun setUp() { harness = Harness() harness.server.on(nsid, thread(json(Fixtures.comment(text = "First one")), total = 4)) }
@After fun tearDown() = harness.tearDown()
private fun viewModel() = StoryCommentsViewModel(harness.client, harness.auth)
@Test fun switchingToAStoryReadsItsPreviewAndCount() { val model = viewModel() model.switchToStory(first)
val state = model.state.awaitValue("the preview") { it.firstComment != null } assertEquals("First one", state.firstComment!!.text) assertEquals(4, state.totalCount) assertEquals("The preview is one comment, not the thread", "1", harness.server.lastCall(nsid).params["limit"]) }
@Test fun switchingToTheStoryAlreadyShowingCostsNothing() { val model = viewModel() model.switchToStory(first) model.state.awaitValue("the preview") { it.firstComment != null }
model.switchToStory(first) settle()
assertEquals(1, harness.server.countOf(nsid)) }
@Test fun pagingBackToAStoryAlreadySeenRedrawsFromTheCache() { val model = viewModel() model.switchToStory(first) model.state.awaitValue("the first preview") { it.firstComment != null }
harness.server.on(nsid, thread(json(Fixtures.comment(text = "Second one")), total = 1)) model.switchToStory(second) model.state.awaitValue("the second preview") { it.firstComment?.text == "Second one" } val fetches = harness.server.countOf(nsid)
model.switchToStory(first)
val back = model.state.awaitValue("the cached preview") { it.firstComment?.text == "First one" } assertEquals(4, back.totalCount) settle() assertEquals("No refetch for a story already seen", fetches, harness.server.countOf(nsid)) }
@Test fun openingTheSheetReadsTheWholeThread() { val model = viewModel() model.switchToStory(first) model.state.awaitValue("the preview") { it.firstComment != null }
harness.server.on( nsid, thread(json(Fixtures.comment()), json(Fixtures.comment(rkey = "c2", text = "Two")), total = 2), ) model.loadComments()
val state = model.state.awaitValue("the thread") { it.hasLoaded } assertEquals(2, state.comments.size) assertEquals(2, state.totalCount) assertNull(harness.server.lastCall(nsid).params["limit"]?.takeIf { it == "1" }) }
@Test fun loadingWithNoStoryOnScreenDoesNothing() { viewModel().loadComments() settle() assertEquals(0, harness.server.countOf(nsid)) }
@Test fun aThreadThatFailsSaysSo() { val model = viewModel() model.switchToStory(first) model.state.awaitValue("the preview") { it.firstComment != null }
harness.server.on(nsid, """{"error":"no"}""", code = 500) model.loadComments()
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals("Couldn't load comments.", state.errorMessage) assertTrue(state.hasLoaded)
model.dismissError() assertNull(model.state.value.errorMessage) }
@Test fun aFailedPreviewLeavesTheBadgeAloneWithoutRaisingAnError() { harness.server.on(nsid, """{"error":"no"}""", code = 500) val model = viewModel() model.switchToStory(first) harness.server.awaitCall(nsid) settle()
assertNull(model.state.value.firstComment) assertNull(model.state.value.errorMessage) }
@Test fun postingACommentInvalidatesTheCachedPreview() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/comment/new"}""") val model = viewModel() model.switchToStory(first) model.state.awaitValue("the preview") { it.firstComment != null }
harness.server.on( nsid, thread(json(Fixtures.comment(text = "Mine")), json(Fixtures.comment(rkey = "c2")), total = 5), ) model.postComment(" Mine ", replyTo = null)
val state = model.state.awaitValue("the reread thread") { it.comments.size == 2 } assertEquals(5, state.totalCount) assertFalse(state.isPosting) assertTrue(harness.server.awaitCall("dev.hatk.createRecord").body.contains("\"text\":\"Mine\"")) }
@Test fun anEmptyCommentIsNotPostedAndNeitherIsOneWithNoStory() { val model = viewModel() model.postComment("Hello", replyTo = null) model.switchToStory(first) model.state.awaitValue("the preview") { it.firstComment != null } model.postComment(" ", 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.switchToStory(first) model.state.awaitValue("the preview") { it.firstComment != null }
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 deletingACommentTakesItsRepliesAndOneOffTheCount() { 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.switchToStory(first) model.loadComments() 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(1, state.totalCount) }
@Test fun aDeleteThatFailsPutsTheRowsBack() { harness.server.on("dev.hatk.deleteRecord", """{"error":"no"}""", code = 500) val model = viewModel() model.switchToStory(first) model.loadComments() val loaded = model.state.awaitValue("the thread") { it.hasLoaded }
model.deleteComment(loaded.comments.first())
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals(1, state.comments.size) }
@Test fun favoritingACommentFlipsItThenKeepsTheRecordUri() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/fav/1"}""") val model = viewModel() model.switchToStory(first) model.loadComments() 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.switchToStory(first) model.loadComments() 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.switchToStory(first) model.loadComments() 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.switchToStory(first) model.loadComments() val state = model.state.awaitValue("the thread") { it.comments.size == 2 }
assertEquals(1, state.threads.size) assertEquals("Reply", state.threads.single().replies.single().text) }}