mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 18:56:44 -04:00
Improved the KiwixSearchView
to handle the keyboard onDone
button press. Now, when the user presses the Done
button on the keyboard, it triggers the search and opens the first matching item, if available.
* Fixed: The `FIND_IN_PAGE` button was appearing when the search was opened from `FromTabView`. * Fixed: The `FIND_IN_PAGE` button was not displaying properly (only half was visible). * Improved: The `NavigationIcon` to better support test cases. * Fixed: `VoiceSearch` was not working when launched from the `SearchWidget`. * Refactored: `SearchFragmentTest` and `SearchFragmentTestForCustomApp` to align with the Compose UI. * Removed: Additional unused code from the project.
This commit is contained in:
parent
a3b1b95181
commit
31cc84664c
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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<RecyclerView.ViewHolder>(
|
||||
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() {
|
||||
|
@ -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<SearchViewModel>(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) {
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -84,7 +84,7 @@ class SearchViewModel @Inject constructor(
|
||||
FromWebView
|
||||
)
|
||||
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
||||
private val _effects = Channel<SideEffect<*>>()
|
||||
private val _effects = Channel<SideEffect<*>>(Channel.UNLIMITED)
|
||||
val effects = _effects.receiveAsFlow()
|
||||
val actions = Channel<Action>(Channel.UNLIMITED)
|
||||
private val filter = MutableStateFlow("")
|
||||
|
@ -46,7 +46,7 @@ data class StartSpeechInput(private val actions: Channel<Action>) : SideEffect<U
|
||||
},
|
||||
REQ_CODE_SPEECH_INPUT
|
||||
)
|
||||
} catch (a: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
actions.trySend(StartSpeechInputFailed).isSuccess
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,6 @@
|
||||
package org.kiwix.kiwixmobile.core.ui.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -37,6 +34,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@ -62,7 +60,6 @@ import org.kiwix.kiwixmobile.core.ui.theme.Black
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray350
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ACTION_MENU_TEXTVIEW_BUTTON_PADDING
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
|
||||
|
||||
const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle"
|
||||
@ -139,30 +136,32 @@ private fun AppBarTitle(
|
||||
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2021 Kiwix <android.kiwix.org>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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<RecyclerView.ViewHolder>(
|
||||
0,
|
||||
ViewActions.click()
|
||||
)
|
||||
)
|
||||
})
|
||||
private fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) {
|
||||
composeTestRule.apply {
|
||||
waitUntilTimeout()
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun assertArticleLoaded() {
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user