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

grain.social/grain-android

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

import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.requestFocus
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
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.feature.explore.ExploreViewModel
import social.grain.feature.search.SearchScreen
import social.grain.feature.search.SearchViewModel
import social.grain.support.Fixtures
import social.grain.support.Harness
import social.grain.support.awaitContentDescription
import social.grain.support.awaitText
import social.grain.support.MainDispatcherRule
import social.grain.support.awaitCall
import social.grain.support.json
import social.grain.ui.theme.GrainTheme

/**
 * Search: explore before anything is typed, recents while the field has focus,
 * and two tabs of results once it does.
 */
@RunWith(AndroidJUnit4::class)
class SearchScreenTest {

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    @get:Rule
    val composeRule = createComposeRule()

    private lateinit var harness: Harness

    @Before
    fun setUp() {
        harness = Harness()
        harness.recentSearches.switchAccount(harness.did)
        harness.recentSearches.clear()
        harness.server
            .on(
                "social.grain.gallery.searchGalleries",
                """{"items":[${json(Fixtures.gallery(title = "Fog on the bay"))}]}""",
            )
            .on(
                "social.grain.actor.searchActors",
                """{"items":[{"did":"did:plc:bob","handle":"bob.test","displayName":"Bob"}]}""",
            )
            .on("social.grain.feed.getFeed", """{"items":[${json(Fixtures.gallery(title = "Explore me"))}]}""")
            .on("social.grain.gallery.getCameras", cameras(2))
            .on(
                "social.grain.gallery.getLocations",
                """{"locations":[{"name":"Lisbon, PT","h3Index":"85390cb7fffffff","galleryCount":3}]}""",
            )
    }

    /** An index of [count] cameras, "Camera 1" upward. */
    private fun cameras(count: Int): String =
        (1..count).joinToString(",", prefix = """{"cameras":[""", postfix = "]}") {
            """{"camera":"Camera $it","photoCount":$it}"""
        }

    @After
    fun tearDown() {
        harness.recentSearches.clear()
        harness.tearDown()
    }

    private lateinit var model: SearchViewModel

    private fun show(
        onOpenProfile: (String) -> Unit = {},
        onOpenGallery: (String) -> Unit = {},
        onOpenCamera: (String) -> Unit = {},
        onOpenPlace: (String, String, Int) -> Unit = { _, _, _ -> },
        onOpenCameras: () -> Unit = {},
        onOpenLocations: () -> Unit = {},
    ) {
        model = SearchViewModel(
            harness.client,
            harness.auth,
            harness.recentSearches,
            harness.labelDefinitions,
        )
        val explore = ExploreViewModel(harness.client, harness.auth, harness.labelDefinitions)
        composeRule.setContent {
            GrainTheme {
                SearchScreen(
                    onOpenProfile = onOpenProfile,
                    onOpenFavorites = {},
                    onOpenPlace = onOpenPlace,
                    onOpenGallery = onOpenGallery,
                    onOpenCamera = onOpenCamera,
                    onOpenCameras = onOpenCameras,
                    onOpenLocations = onOpenLocations,
                    viewModel = model,
                    exploreViewModel = explore,
                )
            }
        }
        composeRule.waitForIdle()
    }

    /** Put the caret in the field, as a tap on it would. */
    private fun focusTheField() {
        composeRule.onNodeWithText("Search galleries and profiles").requestFocus()
        composeRule.waitForIdle()
    }

    @Test
    fun anUntouchedSearchIsTheExplorePage() {
        show()

        composeRule.awaitText("Search galleries and profiles")
        composeRule.awaitText("CAMERAS")
        composeRule.awaitText("Camera 1")
        composeRule.awaitText("LOCATIONS")
        composeRule.awaitText("Lisbon, PT")
        composeRule.awaitContentDescription("Explore me")
    }

    @Test
    fun aCameraChipOpensThatCamerasFeed() {
        var opened: String? = null
        show(onOpenCamera = { opened = it })

        composeRule.awaitText("Camera 2")
        composeRule.onNodeWithText("Camera 2").performClick()

        assertEquals("Camera 2", opened)
    }

    @Test
    fun aLocationChipOpensTheMapOnThatPlace() {
        var opened: Triple<String, String, Int>? = null
        show(onOpenPlace = { cell, name, count -> opened = Triple(cell, name, count) })

        composeRule.awaitText("Lisbon, PT")
        composeRule.onNodeWithText("Lisbon, PT").performClick()

        assertEquals(Triple("85390cb7fffffff", "Lisbon, PT", 3), opened)
    }

    @Test
    fun aThumbnailOpensItsGallery() {
        var opened: String? = null
        show(onOpenGallery = { opened = it })

        composeRule.awaitContentDescription("Explore me")
        composeRule.onNodeWithContentDescription("Explore me").performClick()

        assertEquals(Fixtures.gallery().uri, opened)
    }

    @Test
    fun seeAllOnlyAppearsOnceTheChipsAreTheTopOfALongerList() {
        show()

        composeRule.awaitText("Camera 2")
        composeRule.onNodeWithText("See all").assertDoesNotExist()
    }

    @Test
    fun seeAllOpensTheIndexBehindTheChips() {
        harness.server.on("social.grain.gallery.getCameras", cameras(11))
        var openedCameras = false
        show(onOpenCameras = { openedCameras = true })

        composeRule.awaitText("Camera 10")
        composeRule.onNodeWithText("Camera 11").assertDoesNotExist()
        composeRule.onNodeWithText("See all").performClick()

        assertTrue(openedCameras)
    }

    @Test
    fun typingRunsAGallerySearchAndDrawsTheCards() {
        show()

        composeRule.onNodeWithText("Search galleries and profiles").performTextInput("fog")
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.gallery.searchGalleries") >= 1 }
        composeRule.waitForIdle()

        composeRule.awaitText("Fog on the bay")
    }

    @Test
    fun theProfilesTabRunsItsOwnSearch() {
        show()

        composeRule.onNodeWithText("Search galleries and profiles").performTextInput("bob")
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.gallery.searchGalleries") >= 1 }
        composeRule.waitForIdle()

        composeRule.onNodeWithText("Profiles").performClick()
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.actor.searchActors") >= 1 }
        composeRule.waitForIdle()

        composeRule.awaitText("Bob")
    }

    @Test
    fun aSearchThatFindsNothingSaysSoForTheTabItIsOn() {
        harness.server.on("social.grain.gallery.searchGalleries", """{"items":[]}""")
        show()

        composeRule.onNodeWithText("Search galleries and profiles").performTextInput("nothing")
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.gallery.searchGalleries") >= 1 }
        composeRule.waitForIdle()

        composeRule.awaitText("No galleries match that.")
    }

    @Test
    fun clearingTheFieldPutsThePromptBack() {
        show()

        composeRule.onNodeWithText("Search galleries and profiles").performTextInput("fog")
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.gallery.searchGalleries") >= 1 }
        composeRule.waitForIdle()

        composeRule.onNodeWithContentDescription("Clear search").performClick()
        composeRule.waitForIdle()

        composeRule.awaitText("Camera 1")
    }

    @Test
    fun recentsShowWhileTheFieldHasFocusAndExploreOtherwise() {
        harness.recentSearches.addTerm("fog")
        show()

        composeRule.awaitText("Camera 1")
        composeRule.onNodeWithText("Recent").assertDoesNotExist()

        focusTheField()

        composeRule.awaitText("Recent")
        composeRule.awaitText("fog")
    }

    @Test
    fun rememberedTermsAreOfferedBackAndCanBeRemoved() {
        harness.recentSearches.addTerm("fog")
        show()
        focusTheField()

        composeRule.awaitText("Recent")
        composeRule.awaitText("fog")

        composeRule.onNodeWithContentDescription("Remove from recents").performClick()
        composeRule.waitForIdle()

        composeRule.onNodeWithText("fog").assertDoesNotExist()
    }

    @Test
    fun theWholeRecentsListCanBeCleared() {
        harness.recentSearches.addTerm("fog")
        harness.recentSearches.addTerm("mist")
        show()
        focusTheField()

        composeRule.awaitText("Recent")
        composeRule.onNodeWithText("Clear").performClick()
        composeRule.waitForIdle()

        composeRule.onNodeWithText("Recent").assertDoesNotExist()
    }

    @Test
    fun tappingARememberedTermRunsItAgain() {
        harness.recentSearches.addTerm("fog")
        show()
        focusTheField()

        composeRule.awaitText("fog")
        composeRule.onNodeWithText("fog").performClick()
        composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.gallery.searchGalleries") >= 1 }
        composeRule.waitForIdle()

        composeRule.awaitText("Fog on the bay")
    }
}