Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280package social.grain
import androidx.lifecycle.SavedStateHandleimport androidx.test.ext.junit.runners.AndroidJUnit4import okhttp3.OkHttpClientimport 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.data.location.LocationServicesimport social.grain.feature.feed.CameraFeedViewModelimport social.grain.feature.feed.HashtagFeedViewModelimport social.grain.feature.feed.LocationFeedViewModelimport 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 social.grain.support.testJson
/** * The feeds reached by tapping something on a card — a hashtag, a place name. * * They share a base class, so most of this exercises that: paging, optimistic * favourites, deletes. What the two subclasses add is one line each — which * `feed` parameter goes out — and that line is exactly what a screen test * cannot see. */@RunWith(AndroidJUnit4::class)class GalleryFeedViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private lateinit var harness: Harness
@Before fun setUp() { harness = Harness() harness.server .on("dev.hatk.describeLabels", """{"definitions":[]}""") .on("dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery())}]}""") }
@After fun tearDown() = harness.tearDown()
private fun hashtag(tag: String = "fog") = HashtagFeedViewModel( client = harness.client, auth = harness.auth, labelDefsCache = harness.labelDefinitions, savedStateHandle = SavedStateHandle(mapOf("tag" to tag)), )
private fun location(cell: String = "8928308280fffff", name: String = "Mission District") = LocationFeedViewModel( client = harness.client, auth = harness.auth, locations = LocationServices(OkHttpClient(), testJson), labelDefsCache = harness.labelDefinitions, savedStateHandle = SavedStateHandle(mapOf("cell" to cell, "name" to name)), )
@Test fun aHashtagFeedAsksForThatTag() { val model = hashtag("fog") val state = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() }
assertEquals("fog", model.tag) assertFalse(state.isLoading) assertFalse(state.isEmpty) assertEquals(harness.did, state.viewerDid)
val call = harness.server.lastCall("dev.hatk.getFeed") assertEquals("hashtag", call.params["feed"]) assertEquals("fog", call.params["tag"]) }
@Test fun aLocationFeedAsksForThatCellAndKeepsTheNameItWasGiven() { val model = location() model.state.awaitValue("the first page") { it.galleries.isNotEmpty() }
assertEquals("Mission District", model.locationName) assertEquals("8928308280fffff", model.cell)
val call = harness.server.lastCall("dev.hatk.getFeed") assertEquals("location", call.params["feed"]) assertEquals("8928308280fffff", call.params["location"]) // The index counts a place across every cell it folded in; the name // is what makes the feed cover the same ground. assertEquals("Mission District", call.params["name"]) }
@Test fun aCameraFeedAsksForThatCamera() { val model = CameraFeedViewModel( client = harness.client, auth = harness.auth, labelDefsCache = harness.labelDefinitions, savedStateHandle = SavedStateHandle(mapOf("name" to "Leica M6")), ) model.state.awaitValue("the first page") { it.galleries.isNotEmpty() }
assertEquals("Leica M6", model.camera)
val call = harness.server.lastCall("dev.hatk.getFeed") assertEquals("camera", call.params["feed"]) assertEquals("Leica M6", call.params["camera"]) }
@Test fun anEmptyAnswerIsOnlyReportedAsEmptyOnceItHasAnswered() { harness.server.on("dev.hatk.getFeed", """{"items":[]}""") val model = hashtag()
val state = model.state.awaitValue("the answer") { !it.isLoading } assertTrue(state.isEmpty) }
@Test fun aFailedLoadFallsBackToTheFeedsOwnWording() { harness.server.on("dev.hatk.getFeed", """{"error":"no"}""", code = 500) val model = hashtag()
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals("HTTP error 500", state.errorMessage) assertFalse(state.isRefreshing)
model.dismissError() assertNull(model.state.value.errorMessage) }
@Test fun pullingToRefreshShowsTheIndicatorAndReReads() { val model = hashtag() model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } val before = harness.server.countOf("dev.hatk.getFeed")
model.refresh() harness.server.awaitCall("dev.hatk.getFeed", before + 1) model.state.awaitValue("the refresh to finish") { !it.isRefreshing } }
@Test fun moreGalleriesAppendBehindTheCursor() { harness.server.on( "dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery())}],"cursor":"c1"}""", ) val model = hashtag() model.state.awaitValue("the first page") { it.galleries.isNotEmpty() }
harness.server.on( "dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery(rkey = "g2", title = "Second"))}]}""", ) model.loadMore()
val state = model.state.awaitValue("the second page") { it.galleries.size == 2 } assertEquals("Second", state.galleries[1].title) assertEquals("c1", harness.server.lastCall("dev.hatk.getFeed").params["cursor"]) assertFalse(state.isLoadingMore) }
@Test fun aPageWithNoCursorIsTheEndOfTheFeed() { val model = hashtag() model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } val before = harness.server.countOf("dev.hatk.getFeed")
model.loadMore() settle()
assertEquals(before, harness.server.countOf("dev.hatk.getFeed")) }
@Test fun favoritingFlipsTheHeartThenKeepsTheRecordUri() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/fav/1"}""") val model = hashtag() val gallery = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } .galleries.single()
model.toggleFavorite(gallery.uri)
assertEquals(13, model.state.value.galleries.single().favCount) model.state.awaitValue("the record uri") { it.galleries.single().viewer?.fav == "at://did:plc:me/fav/1" } }
@Test fun aFavoriteThatFailsIsPutBack() { harness.server.on("dev.hatk.createRecord", """{"error":"no"}""", code = 500) val model = hashtag() val gallery = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } .galleries.single()
model.toggleFavorite(gallery.uri)
val state = model.state.awaitValue("the revert") { !it.galleries.single().isFavorited } assertEquals(12, state.galleries.single().favCount) }
@Test fun unfavoritingDeletesTheRecord() { harness.server.on( "dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery(favUri = "at://did:plc:me/social.grain.favorite/f1"))}]}""", ) val model = hashtag() val gallery = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } .galleries.single()
model.toggleFavorite(gallery.uri)
assertTrue(harness.server.awaitCall("dev.hatk.deleteRecord").body.contains("\"rkey\":\"f1\"")) assertEquals(11, model.state.value.galleries.single().favCount) }
@Test fun favoritingSomethingNotInTheFeedIsIgnored() { val model = hashtag() model.state.awaitValue("the first page") { it.galleries.isNotEmpty() }
model.toggleFavorite("at://nothing") settle()
assertEquals(0, harness.server.countOf("dev.hatk.createRecord")) }
@Test fun theCommentCountFollowsTheSheetOpenedOverACard() { val model = hashtag() val gallery = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } .galleries.single()
model.setCommentCount(gallery.uri, 7)
assertEquals(7, model.state.value.galleries.single().commentCount) }
@Test fun deletingAGalleryRemovesTheCardOnlyOnceTheWriteLands() { val model = hashtag() val gallery = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } .galleries.single()
model.deleteGallery(gallery.uri)
model.state.awaitValue("the card to go") { it.galleries.isEmpty() } }
@Test fun aDeleteThatFailsLeavesTheCardAndSaysSo() { harness.server.on( "social.grain.unspecced.deleteGallery", """{"error":"no"}""", code = 500, ) val model = hashtag() val gallery = model.state.awaitValue("the first page") { it.galleries.isNotEmpty() } .galleries.single()
model.deleteGallery(gallery.uri)
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertEquals("Couldn't delete that gallery.", state.errorMessage) assertEquals(1, state.galleries.size) }}