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

grain.social/grain-android

1 collaborator
0
1
main
247 lines · 9.4 KB Raw History
package social.grain

import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import okhttp3.OkHttpClient
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.data.location.LocationServices
import social.grain.feature.explore.CamerasViewModel
import social.grain.feature.explore.LocationsViewModel
import social.grain.support.Fixtures
import social.grain.support.Harness
import social.grain.support.MainDispatcherRule
import social.grain.support.awaitCall
import social.grain.support.awaitValue
import social.grain.support.json
import social.grain.support.settle
import social.grain.support.testJson

/** The two index screens behind explore's "See all". */
@RunWith(AndroidJUnit4::class)
class BrowseIndexViewModelsTest {

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    private lateinit var harness: Harness

    @Before
    fun setUp() {
        harness = Harness()
        harness.server
            .on(
                "social.grain.gallery.getCameras",
                """{"cameras":[{"camera":"Leica M6","photoCount":120,"thumbs":["https://cdn.grain.test/thumb/1.jpg"]}]}""",
            )
            .on("social.grain.gallery.getLocations") { call ->
                if (call.params["pins"] == "true") {
                    """{"locations":[
                        {"name":"Lisbon, PT","h3Index":"85390cb7fffffff","galleryCount":3},
                        {"name":"Porto, PT","h3Index":"853913d3fffffff","galleryCount":1}
                    ]}"""
                } else {
                    """{"locations":[{"name":"Lisbon, PT","h3Index":"85390cb7fffffff","galleryCount":3,"thumbs":["https://cdn.grain.test/thumb/2.jpg"]}]}"""
                }
            }
    }

    @After
    fun tearDown() = harness.tearDown()

    private fun cameras() = CamerasViewModel(harness.client, harness.auth)

    private fun locations(
        services: LocationServices = LocationServices(OkHttpClient(), testJson),
        route: SavedStateHandle = SavedStateHandle(),
    ) = LocationsViewModel(harness.client, harness.auth, services, harness.labelDefinitions, route)

    @Test
    fun camerasArriveWithTheirThumbnails() {
        val model = cameras()
        val state = model.state.awaitValue("the cameras") { !it.isLoading }

        assertEquals("Leica M6", state.items.single().camera)
        assertEquals(1, state.items.single().thumbs.size)
        assertFalse(state.isEmpty)
        assertNull(state.errorMessage)
    }

    @Test
    fun anEmptyCamerasIndexIsEmptyOnlyOnceItHasAnswered() {
        harness.server.on("social.grain.gallery.getCameras", """{"cameras":[]}""")
        val model = cameras()

        assertFalse(model.state.value.isEmpty)
        val state = model.state.awaitValue("the answer") { !it.isLoading }
        assertTrue(state.isEmpty)
    }

    @Test
    fun aFailedCamerasLoadSaysSoAndCanBeDismissed() {
        harness.server.on("social.grain.gallery.getCameras", """{"error":"no"}""", code = 500)
        val model = cameras()

        val state = model.state.awaitValue("the error") { it.errorMessage != null }
        assertEquals("HTTP error 500", state.errorMessage)
        assertFalse(state.isLoading)

        model.dismissError()
        assertNull(model.state.value.errorMessage)
    }

    @Test
    fun refreshingCamerasShowsTheIndicatorAndReReads() {
        val model = cameras()
        model.state.awaitValue("the cameras") { !it.isLoading }
        val before = harness.server.countOf("social.grain.gallery.getCameras")

        model.refresh()

        harness.server.awaitCall("social.grain.gallery.getCameras", before + 1)
        model.state.awaitValue("the refresh to finish") { !it.isRefreshing }
    }

    @Test
    fun locationsAskForTilesAndPinsSeparately() {
        val model = locations()
        val state = model.state.awaitValue("the tiles") { !it.isLoading }

        assertEquals("Lisbon, PT", state.items.single().name)
        assertEquals(1, state.items.single().thumbs.size)

        // The two go out together, so the tiles can answer before the pins
        // request has even been recorded.
        harness.server.awaitCall("social.grain.gallery.getLocations", 2)
        val calls = harness.server.callsTo("social.grain.gallery.getLocations")
        assertEquals(2, calls.size)
        assertEquals(setOf(null, "true"), calls.map { it.params["pins"] }.toSet())
    }

    @Test
    fun everyPlaceWithACoordinateBecomesAPin() {
        val services = LocationServices(OkHttpClient(), testJson)
        val model = locations(services)
        harness.server.awaitCall("social.grain.gallery.getLocations", 2)
        model.state.awaitValue("the tiles") { !it.isLoading }

        // h3 is a native library that only ships for arm; where it can't load
        // there are no coordinates and so no pins, and that is the contract.
        if (services.isAvailable) {
            val pins = model.pins.awaitValue("the pins") { it.size == 2 }
            assertEquals(listOf("Lisbon, PT", "Porto, PT"), pins.map { it.name })
            assertEquals(3, pins.first().galleryCount)
        } else {
            assertTrue(model.pins.value.isEmpty())
        }
    }

    @Test
    fun pickingAPlaceLoadsItsGalleriesWithoutLeavingTheIndex() {
        harness.server.on("social.grain.feed.getFeed", """{"items":[${json(Fixtures.gallery())}],"cursor":"c1"}""")
        val model = locations()
        model.state.awaitValue("the tiles") { !it.isLoading }

        model.select("85390cb7fffffff", "Lisbon, PT", 3)

        val place = model.selected.awaitValue("the galleries") { it?.galleries?.isNotEmpty() == true }!!
        assertEquals("Lisbon, PT", place.name)
        assertEquals(3, place.galleryCount)
        assertFalse(place.isLoading)
        val call = harness.server.lastCall("social.grain.feed.getFeed")
        assertEquals("location", call.params["feed"]?.substringAfterLast('/'))
        assertEquals("85390cb7fffffff", call.params["location"])
        assertEquals("Lisbon, PT", call.params["name"])
        // The index is still there underneath.
        assertEquals(1, model.state.value.items.size)
    }

    @Test
    fun moreOfAPlacesGalleriesAppendBehindTheCursor() {
        harness.server.on("social.grain.feed.getFeed", """{"items":[${json(Fixtures.gallery())}],"cursor":"c1"}""")
        val model = locations()
        model.select("85390cb7fffffff", "Lisbon, PT", 3)
        model.selected.awaitValue("the first page") { it?.galleries?.isNotEmpty() == true }

        harness.server.on(
            "social.grain.feed.getFeed",
            """{"items":[${json(Fixtures.gallery(rkey = "g2", title = "Second"))}]}""",
        )
        model.loadMoreGalleries()

        val place = model.selected.awaitValue("the second page") { it?.galleries?.size == 2 }!!
        assertEquals("Second", place.galleries[1].title)
        assertEquals("c1", harness.server.lastCall("social.grain.feed.getFeed").params["cursor"])
        assertFalse(place.isLoadingMore)
    }

    @Test
    fun pickingThePlaceAlreadyShownDoesNotReload() {
        harness.server.on("social.grain.feed.getFeed", """{"items":[${json(Fixtures.gallery())}]}""")
        val model = locations()
        model.select("85390cb7fffffff", "Lisbon, PT", 3)
        model.selected.awaitValue("the galleries") { it?.galleries?.isNotEmpty() == true }
        val before = harness.server.countOf("social.grain.feed.getFeed")

        model.select("85390cb7fffffff", "Lisbon, PT", 3)
        settle()

        assertEquals(before, harness.server.countOf("social.grain.feed.getFeed"))
    }

    @Test
    fun openedFromAChipTheScreenStartsOnThatPlace() {
        harness.server.on("social.grain.feed.getFeed", """{"items":[${json(Fixtures.gallery())}]}""")
        val model = locations(
            route = SavedStateHandle(
                mapOf("cell" to "85390cb7fffffff", "name" to "Lisbon, PT", "galleryCount" to 3),
            ),
        )

        assertTrue(model.openedOnPlace)
        val place = model.selected.awaitValue("the galleries") { it?.galleries?.isNotEmpty() == true }!!
        assertEquals("Lisbon, PT", place.name)
        assertEquals(3, place.galleryCount)
        assertEquals("85390cb7fffffff", harness.server.lastCall("social.grain.feed.getFeed").params["location"])
    }

    @Test
    fun openedFromSeeAllTheScreenStartsOnTheIndex() {
        val model = locations()
        assertFalse(model.openedOnPlace)
        assertNull(model.selected.value)
    }

    @Test
    fun clearingTheSelectionBringsTheIndexBack() {
        harness.server.on("social.grain.feed.getFeed", """{"items":[]}""")
        val model = locations()
        model.select("85390cb7fffffff", "Lisbon, PT", 3)
        val place = model.selected.awaitValue("the answer") { it?.isLoading == false }!!
        assertTrue(place.isEmpty)

        model.clearSelection()

        assertNull(model.selected.value)
    }

    @Test
    fun aFailedLocationsLoadSaysSo() {
        harness.server.on("social.grain.gallery.getLocations", """{"error":"no"}""", code = 500)
        val model = locations()

        val state = model.state.awaitValue("the error") { it.errorMessage != null }
        assertEquals("HTTP error 500", state.errorMessage)
        assertTrue(model.pins.value.isEmpty())

        model.dismissError()
        assertNull(model.state.value.errorMessage)
    }
}