diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt index acbc23d3f..edbbaaa2f 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt @@ -31,6 +31,7 @@ import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG import org.kiwix.kiwixmobile.language.composables.LANGUAGE_ITEM_CHECKBOX_TESTING_TAG import org.kiwix.kiwixmobile.nav.destination.library.online.LANGUAGE_MENU_ICON_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt index 6a718ff71..de409f415 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.search import android.os.Build +import androidx.compose.ui.test.junit4.createComposeRule import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.Lifecycle @@ -25,7 +26,6 @@ import androidx.navigation.fragment.NavHostFragment import androidx.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.espresso.accessibility.AccessibilityChecks -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.platform.app.InstrumentationRegistry @@ -47,11 +47,11 @@ import org.junit.Rule import org.junit.Test import org.kiwix.kiwixmobile.BaseActivityTest import org.kiwix.kiwixmobile.R -import org.kiwix.kiwixmobile.core.R.id import org.kiwix.kiwixmobile.core.search.SearchFragment import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader @@ -73,6 +73,9 @@ class SearchFragmentTest : BaseActivityTest() { @JvmField val retryRule = RetryRule() + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + private lateinit var kiwixMainActivity: KiwixMainActivity private lateinit var uiDevice: UiDevice private lateinit var downloadingZimFile: File @@ -115,10 +118,6 @@ class SearchFragmentTest : BaseActivityTest() { setRunChecksFromRootView(true) setSuppressingResultMatcher( anyOf( - allOf( - matchesCheck(TouchTargetSizeCheck::class.java), - matchesViews(ViewMatchers.withId(id.menu_searchintext)) - ), allOf( matchesCheck(TouchTargetSizeCheck::class.java), matchesViews( @@ -142,28 +141,32 @@ class SearchFragmentTest : BaseActivityTest() { search { checkZimFileSearchSuccessful(R.id.readerFragment) } openSearchWithQuery("Android", testZimFile) search { - clickOnSearchItemInSearchList() + clickOnSearchItemInSearchList(composeTestRule) checkZimFileSearchSuccessful(R.id.readerFragment) } openSearchWithQuery(zimFile = testZimFile) search { // test with fast typing/deleting - searchWithFrequentlyTypedWords(searchUnitTestingQuery) - assertSearchSuccessful(searchUnitTestResult) - deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice) + searchWithFrequentlyTypedWords(searchUnitTestingQuery, composeTestRule = composeTestRule) + assertSearchSuccessful(searchUnitTestResult, composeTestRule) + deleteSearchedQueryFrequently( + searchUnitTestingQuery, + uiDevice, + composeTestRule = composeTestRule + ) // test with a short delay typing/deleting to // properly test the cancelling of previously searching task - searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50) - assertSearchSuccessful(searchUnitTestResult) - deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50) + searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50, composeTestRule) + assertSearchSuccessful(searchUnitTestResult, composeTestRule) + deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50, composeTestRule) // test with a long delay typing/deleting to // properly execute the search query letter by letter - searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300) - assertSearchSuccessful(searchUnitTestResult) - deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300) + searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300, composeTestRule) + assertSearchSuccessful(searchUnitTestResult, composeTestRule) + deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300, composeTestRule) // to close the keyboard pressBack() // go to reader screen @@ -190,39 +193,46 @@ class SearchFragmentTest : BaseActivityTest() { openSearchWithQuery(zimFile = downloadingZimFile) search { // test with fast typing/deleting - searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile) - assertSearchSuccessful(searchResultForDownloadedZimFile) - deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice) + searchWithFrequentlyTypedWords( + searchQueryForDownloadedZimFile, + composeTestRule = composeTestRule + ) + assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule) + deleteSearchedQueryFrequently( + searchQueryForDownloadedZimFile, + uiDevice, + composeTestRule = composeTestRule + ) // test with a short delay typing/deleting to // properly test the cancelling of previously searching task - searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50) - assertSearchSuccessful(searchResultForDownloadedZimFile) - deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50) + searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50, composeTestRule) + assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule) + deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50, composeTestRule) // test with a long delay typing/deleting to // properly execute the search query letter by letter - searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300) - assertSearchSuccessful(searchResultForDownloadedZimFile) - deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300) + searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300, composeTestRule) + assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule) + deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300, composeTestRule) // open the reader fragment for next text case. - openKiwixReaderFragmentWithFile(downloadingZimFile) + clickOnNavigationIcon(composeTestRule) } // Added test for checking the crash scenario where the application was crashing when we // frequently searched for article, and clicked on the searched item. search { // test by searching 10 article and clicking on them - searchAndClickOnArticle(searchQueryForDownloadedZimFile) - searchAndClickOnArticle("A Song") - searchAndClickOnArticle("The Ra") - searchAndClickOnArticle("The Ge") - searchAndClickOnArticle("Wish") - searchAndClickOnArticle("WIFI") - searchAndClickOnArticle("Woman") - searchAndClickOnArticle("Big Ba") - searchAndClickOnArticle("My Wor") - searchAndClickOnArticle("100") + searchAndClickOnArticle(searchQueryForDownloadedZimFile, composeTestRule) + searchAndClickOnArticle("A Song", composeTestRule) + searchAndClickOnArticle("The Ra", composeTestRule) + searchAndClickOnArticle("The Ge", composeTestRule) + searchAndClickOnArticle("Wish", composeTestRule) + searchAndClickOnArticle("WIFI", composeTestRule) + searchAndClickOnArticle("Woman", composeTestRule) + searchAndClickOnArticle("Big Ba", composeTestRule) + searchAndClickOnArticle("My Wor", composeTestRule) + searchAndClickOnArticle("100", composeTestRule) assertArticleLoaded() } removeTemporaryZimFilesToFreeUpDeviceStorage() diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt index 750da7f9f..ba2509ed8 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt @@ -19,28 +19,33 @@ package org.kiwix.kiwixmobile.search import android.view.KeyEvent -import androidx.recyclerview.widget.RecyclerView +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.Locator import androidx.test.uiautomator.UiDevice import applyWithViewHierarchyPrinting import com.adevinta.android.barista.interaction.BaristaSleepInteractions -import com.adevinta.android.barista.internal.matcher.HelperMatchers.atPosition import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG +import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG +import org.kiwix.kiwixmobile.core.ui.components.NAVIGATION_ICON_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView +import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout fun search(func: SearchRobot.() -> Unit) = SearchRobot().applyWithViewHierarchyPrinting(func) @@ -50,15 +55,11 @@ class SearchRobot : BaseRobot() { val searchQueryForDownloadedZimFile = "A Fool" val searchResultForDownloadedZimFile = "A Fool for You" - fun clickOnSearchItemInSearchList() { - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - isVisible(ViewId(R.id.search_list)) - onView(withId(R.id.search_list)).perform( - actionOnItemAtPosition( - 0, - click() - ) - ) + fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntilTimeout() + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick() + } } fun checkZimFileSearchSuccessful(readerFragment: Int) { @@ -66,31 +67,47 @@ class SearchRobot : BaseRobot() { isVisible(ViewId(readerFragment)) } - fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) { + fun searchWithFrequentlyTypedWords( + query: String, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { testFlakyView({ - val searchView = onView(withId(androidx.appcompat.R.id.search_src_text)) - for (char in query) { - searchView.perform(typeText(char.toString())) - if (wait != 0L) { - BaristaSleepInteractions.sleep(wait) + composeTestRule.apply { + waitUntilTimeout() + val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG) + searchView.performTextInput("") + for (char in query) { + searchView.performTextInput(char.toString()) + if (wait != 0L) { + waitUntilTimeout(wait) + } } } }) } - fun assertSearchSuccessful(searchResult: String) { - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - val recyclerViewId = R.id.search_list - - onView(withId(recyclerViewId)).check( - matches( - atPosition(0, hasDescendant(withText(searchResult))) + fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntil( + timeoutMillis = TEST_PAUSE_MS.toLong(), + condition = { + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG) + .fetchSemanticsNodes().isNotEmpty() + } ) - ) + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0] + .assert(hasText(searchResult)) + } } - fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) { - for (i in textToDelete.indices) { + fun deleteSearchedQueryFrequently( + textToDelete: String, + uiDevice: UiDevice, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { + repeat(textToDelete.length) { uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL) if (wait != 0L) { BaristaSleepInteractions.sleep(wait) @@ -98,20 +115,22 @@ class SearchRobot : BaseRobot() { } // clear search query if any remains due to any condition not to affect any other test scenario - val searchView = onView(withId(androidx.appcompat.R.id.search_src_text)) - searchView.perform(clearText()) + composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance() + } + + fun clickOnNavigationIcon(composeTestRule: ComposeContentTestRule) { + composeTestRule.onNodeWithTag(NAVIGATION_ICON_TESTING_TAG).performClick() } private fun openSearchScreen() { testFlakyView({ onView(withId(R.id.menu_search)).perform(click()) }) } - fun searchAndClickOnArticle(searchString: String) { - // wait a bit to properly load the ZIM file in the reader - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) + fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) { openSearchScreen() - searchWithFrequentlyTypedWords(searchString) - clickOnSearchItemInSearchList() + searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule) + clickOnSearchItemInSearchList(composeTestRule) + checkZimFileSearchSuccessful(org.kiwix.kiwixmobile.R.id.readerFragment) } fun assertArticleLoaded() { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt index 8125c8a57..4fd16a5de 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt @@ -20,16 +20,11 @@ package org.kiwix.kiwixmobile.core.search import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView -import androidx.core.view.MenuHost -import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -58,6 +53,7 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemClick import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemLongClick import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnOpenInNewTabClick +import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView import org.kiwix.kiwixmobile.core.search.viewmodel.SearchState import org.kiwix.kiwixmobile.core.search.viewmodel.SearchViewModel import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon @@ -79,7 +75,15 @@ class SearchFragment : BaseFragment() { val searchViewModel by lazy { viewModel(viewModelFactory) } private var isDataLoading = mutableStateOf(false) private var renderingJob: Job? = null - private var isFindInPageMenuItemEnabled = mutableStateOf(false) + + /** + * Represents the state of the FIND_IN_PAGE menu item. + * + * A [Pair] containing: + * - [Boolean]: Whether the menu item is enabled (clickable). + * - [Boolean]: Whether the menu item is visible. + */ + private var findInPageMenuItem = mutableStateOf(false to false) private var composeView: ComposeView? = null private val searchScreenState = mutableStateOf( SearchScreenState( @@ -97,7 +101,10 @@ class SearchFragment : BaseFragment() { navigationIcon = { NavigationIcon(onClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }) }, - onLoadMore = { loadMoreSearchResult() } + onLoadMore = { loadMoreSearchResult() }, + onKeyboardSubmitButtonClick = { + getSearchListItemForQuery(it)?.let(::onItemClick) + } ) ) @@ -125,10 +132,10 @@ class SearchFragment : BaseFragment() { DialogHost(dialogShower as AlertDialogShower) } } + searchViewModel.setAlertDialogShower(dialogShower as AlertDialogShower) observeViewModelData() handleSearchArgument() handleBackPress() - searchViewModel.setAlertDialogShower(dialogShower as AlertDialogShower) } private fun handleSearchArgument() { @@ -136,15 +143,18 @@ class SearchFragment : BaseFragment() { if (searchStringFromArguments != null) { onSearchValueChanged(searchStringFromArguments) } - searchViewModel.actions.trySend(Action.CreatedWithArguments(arguments)).isSuccess + val argsCopy = Bundle(arguments) + searchViewModel.actions.trySend(Action.CreatedWithArguments(argsCopy)).isSuccess arguments?.remove(EXTRA_IS_WIDGET_VOICE) } private fun observeViewModelData() { viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + Log.e("VOICE_SEARCH", "Starting observeViewModelData") + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { searchViewModel.effects.collect { effect -> + Log.e("VOICE_SEARCH", "Collected effect: ${effect::class.simpleName}") effect.invokeWith(this@SearchFragment.coreMainActivity) } } @@ -221,41 +231,6 @@ class SearchFragment : BaseFragment() { findNavController().popBackStack(readerFragmentResId, false) } - private fun setupMenu() { - (requireActivity() as MenuHost).addMenuProvider( - object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - // menuInflater.inflate(R.menu.menu_search, menu) - // searchView?.apply { - // setUpSearchView(requireActivity()) - // searchView?.setOnQueryTextListener( - // SimpleTextListener { query, isSubmit -> - // if (query.isNotEmpty()) { - // setIsPageSearchEnabled(true) - // when { - // isSubmit -> { - // // if user press the search/enter button on keyboard, - // // try to open the article if present - // getSearchListItemForQuery(query)?.let(::onItemClick) - // } - // - // else -> searchViewModel.actions.trySend(Filter(query)).isSuccess - // } - // } else { - // setIsPageSearchEnabled(false) - // } - // } - // ) - // } - } - - override fun onMenuItemSelected(menuItem: MenuItem) = true - }, - viewLifecycleOwner, - Lifecycle.State.RESUMED - ) - } - private fun getSearchListItemForQuery(query: String): SearchListItem? = searchScreenState.value.searchList.firstOrNull { it.value.equals(query, ignoreCase = true) @@ -277,16 +252,23 @@ class SearchFragment : BaseFragment() { searchViewModel.actions.trySend(Filter(searchText)).isSuccess } - private fun actionMenuItems() = listOf( - ActionMenuItem( - contentDescription = R.string.menu_search_in_text, - onClick = { - searchViewModel.actions.trySend(ClickedSearchInText).isSuccess - }, - testingTag = FIND_IN_PAGE_TESTING_TAG, - iconButtonText = R.string.menu_search_in_text, - isEnabled = isFindInPageMenuItemEnabled.value - ) + private fun actionMenuItems() = listOfNotNull( + // Check if the `FIND_IN_PAGE` is visible or not. + // If visible then show it in menu. + if (findInPageMenuItem.value.second) { + ActionMenuItem( + contentDescription = R.string.menu_search_in_text, + onClick = { + searchViewModel.actions.trySend(ClickedSearchInText).isSuccess + }, + testingTag = FIND_IN_PAGE_TESTING_TAG, + iconButtonText = R.string.menu_search_in_text, + isEnabled = findInPageMenuItem.value.first + ) + } else { + // If `FIND_IN_PAGE` is not visible return null so that it will not show on the menu item. + null + } ) @Suppress("InjectDispatcher") @@ -310,7 +292,7 @@ class SearchFragment : BaseFragment() { return } isDataLoading.value = false - // searchInTextMenuItem?.actionView?.isVisible = state.searchOrigin == FromWebView + findInPageMenuItem.value = findInPageMenuItem.value.first to (state.searchOrigin == FromWebView) setIsPageSearchEnabled(state.searchTerm) searchScreenState.update { copy(isLoading = true) } renderingJob = @@ -341,7 +323,7 @@ class SearchFragment : BaseFragment() { } private fun setIsPageSearchEnabled(searchText: String) { - isFindInPageMenuItemEnabled.value = searchText.isNotBlank() + findInPageMenuItem.value = searchText.isNotBlank() to findInPageMenuItem.value.second } private fun onItemClick(it: SearchListItem) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt index 6ef06b626..c1d10dd44 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt @@ -69,9 +69,10 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SEARCH_ITEM_TEXT_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP -const val SEARCH_FIELD_TESTING_TAG = "searchField" +const val SEARCH_FIELD_TESTING_TAG = "searchFieldTestingTag" const val NO_SEARCH_RESULT_TESTING_TAG = "noSearchResultTestingTag" const val FIND_IN_PAGE_TESTING_TAG = "findInPageTestingTag" +const val SEARCH_ITEM_TESTING_TAG = "searchItemTestingTag" const val LOADING_ITEMS_BEFORE = 3 @OptIn(ExperimentalMaterial3Api::class) @@ -95,7 +96,8 @@ fun SearchScreen( searchViewTextFiledTestTag = SEARCH_FIELD_TESTING_TAG, onValueChange = searchScreenState.onSearchViewValueChange, onClearClick = searchScreenState.onSearchViewClearClick, - modifier = Modifier + modifier = Modifier, + onKeyboardSubmitButtonClick = searchScreenState.onKeyboardSubmitButtonClick ) } ) @@ -223,7 +225,8 @@ private fun SearchListItem( .combinedClickable( onClick = { onItemClick(searchListItem) }, onLongClick = { onItemLongClick?.invoke(searchListItem) } - ), + ) + .semantics { testTag = SEARCH_ITEM_TESTING_TAG }, fontSize = SEARCH_ITEM_TEXT_SIZE, ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt index 37cd3e22c..9b9e77eca 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt @@ -61,6 +61,10 @@ data class SearchScreenState( * Handles the newTabIcon click. */ val onNewTabIconClick: (SearchListItem) -> Unit, + /** + * Handles the Keyboard submit button click. + */ + val onKeyboardSubmitButtonClick: (String) -> Unit, /** * Manages the navigationIcon shown in the toolbar. */ diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt index ef3e56a48..4ce1d1214 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt @@ -84,7 +84,7 @@ class SearchViewModel @Inject constructor( FromWebView ) val state: MutableStateFlow = MutableStateFlow(initialState) - private val _effects = Channel>() + private val _effects = Channel>(Channel.UNLIMITED) val effects = _effects.receiveAsFlow() val actions = Channel(Channel.UNLIMITED) private val filter = MutableStateFlow("") diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt index 177ffea9f..d35e8e649 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt @@ -46,7 +46,7 @@ data class StartSpeechInput(private val actions: Channel) : SideEffect) { Row { actionMenuItems.forEach { menuItem -> - IconButton( - enabled = menuItem.isEnabled, - onClick = menuItem.onClick, - modifier = menuItem.modifier.testTag(menuItem.testingTag) - ) { - // If icon is not null show the icon. - menuItem.icon?.let { + val modifier = menuItem.modifier.testTag(menuItem.testingTag) + // If icon is not null show the icon. + menuItem.icon?.let { + IconButton( + enabled = menuItem.isEnabled, + onClick = menuItem.onClick, + modifier = modifier + ) { Icon( painter = it.toPainter(), contentDescription = stringResource(menuItem.contentDescription), tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray ) - } ?: run { - // Else show the textView button in menuItem. + } + } ?: run { + // Else show the textView button in menuItem. + TextButton( + enabled = menuItem.isEnabled, + onClick = menuItem.onClick, + modifier = modifier + ) { Text( text = stringResource(id = menuItem.iconButtonText).uppercase(), - color = Color.White, - modifier = menuItem.modifier - .clickable( - indication = LocalIndication.current, - interactionSource = remember { MutableInteractionSource() }, - onClick = menuItem.onClick - ) - .padding(ACTION_MENU_TEXTVIEW_BUTTON_PADDING), + color = if (menuItem.isEnabled) Color.White else Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt index cace7b3b0..b71248743 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt @@ -18,6 +18,8 @@ package org.kiwix.kiwixmobile.core.ui.components +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -30,10 +32,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.utils.ComposeDimens @@ -45,8 +49,10 @@ fun KiwixSearchView( searchViewTextFiledTestTag: String = "", clearButtonTestTag: String = "", onValueChange: (String) -> Unit, - onClearClick: () -> Unit + onClearClick: () -> Unit, + onKeyboardSubmitButtonClick: (String) -> Unit = {} ) { + val keyboardController = LocalSoftwareKeyboardController.current val colors = TextFieldDefaults.colors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, @@ -89,6 +95,15 @@ fun KiwixSearchView( ) } } - } + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + onKeyboardSubmitButtonClick.invoke(value) + } + ) ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt index c652ca8ed..6ab8fde87 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt @@ -24,13 +24,18 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.models.toPainter import org.kiwix.kiwixmobile.core.ui.theme.White +const val NAVIGATION_ICON_TESTING_TAG = "navigationIconTestingTag" + /** * A composable function that renders a navigation icon, which can be either a vector * or drawable image. @@ -47,9 +52,10 @@ fun NavigationIcon( iconItem: IconItem = IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack), onClick: () -> Unit, @StringRes contentDescription: Int = R.string.toolbar_back_button_content_description, - iconTint: Color = White + iconTint: Color = White, + testingTag: String = NAVIGATION_ICON_TESTING_TAG ) { - IconButton(onClick = onClick) { + IconButton(onClick = onClick, modifier = Modifier.semantics { testTag = testingTag }) { Icon( painter = iconItem.toPainter(), contentDescription = stringResource(contentDescription), diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleRecyclerViewScrollListener.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleRecyclerViewScrollListener.kt deleted file mode 100644 index 7c4d307d9..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleRecyclerViewScrollListener.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2021 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.core.utils - -import androidx.recyclerview.widget.RecyclerView - -class SimpleRecyclerViewScrollListener( - private val onLayoutScrollListener: (RecyclerView, Int) -> Unit -) : - RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) - onLayoutScrollListener( - recyclerView, - newState - ) - } - - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - val currentScrollPosition = recyclerView.computeVerticalScrollOffset() - - if (currentScrollPosition > previousScrollPosition) { - onLayoutScrollListener( - recyclerView, - SCROLL_DOWN - ) - } else if (currentScrollPosition < previousScrollPosition) { - onLayoutScrollListener( - recyclerView, - SCROLL_UP - ) - } - - previousScrollPosition = currentScrollPosition - } - - private var previousScrollPosition = 0 - - companion object { - const val SCROLL_DOWN = 2000 - const val SCROLL_UP = 2001 - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextListener.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextListener.kt deleted file mode 100644 index b94abcf24..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextListener.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2020 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.core.utils - -import androidx.appcompat.widget.SearchView.OnQueryTextListener - -class SimpleTextListener(private val onQueryTextChangeAction: (String, Boolean) -> Unit) : - OnQueryTextListener { - override fun onQueryTextSubmit(s: String): Boolean { - onQueryTextChangeAction.invoke(s, true) - return true - } - - override fun onQueryTextChange(s: String): Boolean { - onQueryTextChangeAction.invoke(s, false) - return true - } -} diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt index a9a3f1d9a..d44009ea1 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt @@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.custom.search import android.Manifest import android.content.Context import android.content.res.AssetFileDescriptor +import androidx.compose.ui.test.junit4.createComposeRule import androidx.core.content.edit import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.NavHostFragment @@ -52,6 +53,8 @@ import org.kiwix.kiwixmobile.core.search.SearchFragment import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER +import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER import org.kiwix.kiwixmobile.custom.main.CustomMainActivity import org.kiwix.kiwixmobile.custom.main.CustomReaderFragment import org.kiwix.kiwixmobile.custom.testutils.RetryRule @@ -80,10 +83,13 @@ class SearchFragmentTestForCustomApp { InstrumentationRegistry.getInstrumentation().targetContext.applicationContext } - @Rule + @Rule(order = RETRY_RULE_ORDER) @JvmField var retryRule = RetryRule() + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + private lateinit var customMainActivity: CustomMainActivity private lateinit var uiDevice: UiDevice private lateinit var downloadingZimFile: File @@ -151,24 +157,24 @@ class SearchFragmentTestForCustomApp { openZimFileInReader(zimFile = downloadingZimFile) openSearchWithQuery() val searchTerm = "gard" - val searchedItem = "Gardanta Spirito" + val searchedItem = "Gardanta Spirito - Андивионский Научный Альянс" search { // test with fast typing/deleting - searchWithFrequentlyTypedWords(searchTerm) - assertSearchSuccessful(searchedItem) - deleteSearchedQueryFrequently(searchTerm, uiDevice) + searchWithFrequentlyTypedWords(searchTerm, composeTestRule = composeTestRule) + assertSearchSuccessful(searchedItem, composeTestRule) + deleteSearchedQueryFrequently(searchTerm, uiDevice, composeTestRule = composeTestRule) // test with a short delay typing/deleting to // properly test the cancelling of previously searching task - searchWithFrequentlyTypedWords(searchTerm, 50) - assertSearchSuccessful(searchedItem) - deleteSearchedQueryFrequently(searchTerm, uiDevice, 50) + searchWithFrequentlyTypedWords(searchTerm, 50, composeTestRule) + assertSearchSuccessful(searchedItem, composeTestRule) + deleteSearchedQueryFrequently(searchTerm, uiDevice, 50, composeTestRule) // test with a long delay typing/deleting to // properly execute the search query letter by letter - searchWithFrequentlyTypedWords(searchTerm, 300) - assertSearchSuccessful(searchedItem) - deleteSearchedQueryFrequently(searchTerm, uiDevice, 300) + searchWithFrequentlyTypedWords(searchTerm, 300, composeTestRule) + assertSearchSuccessful(searchedItem, composeTestRule) + deleteSearchedQueryFrequently(searchTerm, uiDevice, 300, composeTestRule) // to close the keyboard pressBack() // go to reader screen @@ -179,16 +185,16 @@ class SearchFragmentTestForCustomApp { // frequently searched for article, and clicked on the searched item. search { // test by searching 10 article and clicking on them - searchAndClickOnArticle(searchTerm) - searchAndClickOnArticle("eilum") - searchAndClickOnArticle("page") - searchAndClickOnArticle("list") - searchAndClickOnArticle("ladder") - searchAndClickOnArticle("welc") - searchAndClickOnArticle("js") - searchAndClickOnArticle("hizo") - searchAndClickOnArticle("fad") - searchAndClickOnArticle("forum") + searchAndClickOnArticle(searchTerm, composeTestRule) + searchAndClickOnArticle("eilum", composeTestRule) + searchAndClickOnArticle("page", composeTestRule) + searchAndClickOnArticle("list", composeTestRule) + searchAndClickOnArticle("ladder", composeTestRule) + searchAndClickOnArticle("welc", composeTestRule) + searchAndClickOnArticle("js", composeTestRule) + searchAndClickOnArticle("hizo", composeTestRule) + searchAndClickOnArticle("fad", composeTestRule) + searchAndClickOnArticle("forum", composeTestRule) assertArticleLoaded() } } diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt index a266d0d07..8828c047f 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt @@ -19,14 +19,19 @@ package org.kiwix.kiwixmobile.custom.search import android.view.KeyEvent +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.core.view.GravityCompat -import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches @@ -40,56 +45,68 @@ import androidx.test.espresso.web.webdriver.Locator import androidx.test.uiautomator.UiDevice import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawerWithGravity import com.adevinta.android.barista.interaction.BaristaSleepInteractions -import com.adevinta.android.barista.internal.matcher.HelperMatchers import org.hamcrest.CoreMatchers.containsString import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG +import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG import org.kiwix.kiwixmobile.custom.R.id import org.kiwix.kiwixmobile.custom.testutils.TestUtils +import org.kiwix.kiwixmobile.custom.testutils.TestUtils.TEST_PAUSE_MS import org.kiwix.kiwixmobile.custom.testutils.TestUtils.testFlakyView +import org.kiwix.kiwixmobile.custom.testutils.TestUtils.waitUntilTimeout fun search(searchRobot: SearchRobot.() -> Unit) = SearchRobot().searchRobot() class SearchRobot { - fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) { + fun searchWithFrequentlyTypedWords( + query: String, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { testFlakyView({ - val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text)) - searchView.perform(ViewActions.clearText()) - for (char in query) { - searchView.perform(ViewActions.typeText(char.toString())) - if (wait != 0L) { - BaristaSleepInteractions.sleep(wait) + composeTestRule.apply { + waitUntilTimeout() + val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG) + searchView.performTextInput("") + for (char in query) { + searchView.performTextInput(char.toString()) + if (wait != 0L) { + waitUntilTimeout(wait) + } } } }) } - fun assertSearchSuccessful(searchResult: String) { - testFlakyView({ - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - Espresso.onView(ViewMatchers.withId(R.id.search_list)).check( - ViewAssertions.matches( - HelperMatchers.atPosition( - 0, - ViewMatchers.hasDescendant(ViewMatchers.withSubstring(searchResult)) - ) - ) + fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntil( + timeoutMillis = TEST_PAUSE_MS.toLong(), + condition = { + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG) + .fetchSemanticsNodes().isNotEmpty() + } ) - }) + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0] + .assert(hasText(searchResult)) + } } - fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) { - testFlakyView({ - for (i in textToDelete.indices) { - uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL) - if (wait != 0L) { - BaristaSleepInteractions.sleep(wait) - } + fun deleteSearchedQueryFrequently( + textToDelete: String, + uiDevice: UiDevice, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { + repeat(textToDelete.length) { + uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL) + if (wait != 0L) { + BaristaSleepInteractions.sleep(wait) } + } - // clear search query if any remains due to any condition not to affect any other test scenario - val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text)) - searchView.perform(ViewActions.clearText()) - }) + // clear search query if any remains due to any condition not to affect any other test scenario + composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance() } private fun openSearchScreen() { @@ -99,26 +116,21 @@ class SearchRobot { }) } - fun searchAndClickOnArticle(searchString: String) { - // Wait a bit to properly load the ZIM file in the reader. - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) + fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) { + // wait a bit to properly load the ZIM file in the reader + composeTestRule.waitUntilTimeout(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) openSearchScreen() // Wait a bit to properly visible the search screen. - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - searchWithFrequentlyTypedWords(searchString) - clickOnSearchItemInSearchList() + composeTestRule.waitUntilTimeout(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) + searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule) + clickOnSearchItemInSearchList(composeTestRule) } - private fun clickOnSearchItemInSearchList() { - testFlakyView({ - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - Espresso.onView(ViewMatchers.withId(R.id.search_list)).perform( - RecyclerViewActions.actionOnItemAtPosition( - 0, - ViewActions.click() - ) - ) - }) + private fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntilTimeout() + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick() + } } fun assertArticleLoaded() { diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt index de9df8ac3..671d823d3 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt @@ -21,12 +21,15 @@ package org.kiwix.kiwixmobile.custom.testutils import android.content.Context import android.content.ContextWrapper import android.content.Intent +import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiSelector import org.kiwix.kiwixmobile.core.utils.files.Log import java.io.File +import java.util.Timer +import java.util.TimerTask object TestUtils { private const val TAG = "TESTUTILS" @@ -90,6 +93,31 @@ object TestUtils { } } + fun ComposeContentTestRule.waitUntilTimeout(timeoutMillis: Long = TEST_PAUSE_MS.toLong()) { + AsyncTimer.start(timeoutMillis) + waitUntil( + condition = { AsyncTimer.expired }, + timeoutMillis = timeoutMillis + 1000 + ) + } + + object AsyncTimer { + var expired = false + fun start(delay: Long = 1000) { + expired = false + val timerTask = TimerTaskImpl { + expired = true + } + Timer().schedule(timerTask, delay) + } + } + + class TimerTaskImpl(private val runnable: Runnable) : TimerTask() { + override fun run() { + runnable.run() + } + } + @JvmStatic fun deleteTemporaryFilesOfTestCases(context: Context) { context.getExternalFilesDirs(null).filterNotNull()