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("dev.hatk.describeLabels", """{"definitions":[]}""") .on( "social.grain.unspecced.searchGalleries", """{"items":[${json(Fixtures.gallery(title = "Fog on the bay"))}]}""", ) .on( "social.grain.unspecced.searchProfiles", """{"items":[{"did":"did:plc:bob","handle":"bob.test","displayName":"Bob"}]}""", ) .on("dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery(title = "Explore me"))}]}""") .on("social.grain.unspecced.getCameras", cameras(2)) .on( "social.grain.unspecced.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? = 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.unspecced.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.unspecced.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.unspecced.searchGalleries") >= 1 } composeRule.waitForIdle() composeRule.onNodeWithText("Profiles").performClick() composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.unspecced.searchProfiles") >= 1 } composeRule.waitForIdle() composeRule.awaitText("Bob") } @Test fun aSearchThatFindsNothingSaysSoForTheTabItIsOn() { harness.server.on("social.grain.unspecced.searchGalleries", """{"items":[]}""") show() composeRule.onNodeWithText("Search galleries and profiles").performTextInput("nothing") composeRule.waitUntil(10_000) { harness.server.countOf("social.grain.unspecced.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.unspecced.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.unspecced.searchGalleries") >= 1 } composeRule.waitForIdle() composeRule.awaitText("Fog on the bay") } }