Merge pull request #4201 from kiwix/Fixes#4200

Fixed: `FIND_IN_PAGE` feature only works with first tab page.
This commit is contained in:
Kelson 2025-02-01 12:09:52 +01:00 committed by GitHub
commit 6e3ceacd26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 151 additions and 26 deletions

View File

@ -220,7 +220,7 @@ object TestUtils {
@JvmStatic @JvmStatic
fun deleteTemporaryFilesOfTestCases(context: Context) { fun deleteTemporaryFilesOfTestCases(context: Context) {
ContextCompat.getExternalFilesDirs(context, null).filterNotNull() context.getExternalFilesDirs(null).filterNotNull()
.map(::deleteAllFilesInDirectory) .map(::deleteAllFilesInDirectory)
ContextWrapper(context).externalMediaDirs.filterNotNull() ContextWrapper(context).externalMediaDirs.filterNotNull()
.map(::deleteAllFilesInDirectory) .map(::deleteAllFilesInDirectory)

View File

@ -505,16 +505,6 @@ abstract class CoreReaderFragment :
readAloudService?.registerCallBack(this@CoreReaderFragment) readAloudService?.registerCallBack(this@CoreReaderFragment)
} }
} }
requireActivity().observeNavigationResult<String>(
FIND_IN_PAGE_SEARCH_STRING,
viewLifecycleOwner,
Observer(::findInPage)
)
requireActivity().observeNavigationResult<SearchItemToOpen>(
TAG_FILE_SEARCHED,
viewLifecycleOwner,
Observer(::openSearchItem)
)
handleClicks() handleClicks()
} }
@ -2617,6 +2607,28 @@ abstract class CoreReaderFragment :
Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", e) Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", e)
activity.toast(R.string.could_not_restore_tabs, Toast.LENGTH_LONG) activity.toast(R.string.could_not_restore_tabs, Toast.LENGTH_LONG)
} }
// After restoring the tabs, observe any search actions that the user might have triggered.
// Since the ZIM file opening functionality has been moved to a background thread,
// we ensure that all necessary actions are completed before observing these search actions.
observeSearchActions()
}
/**
* Observes any search-related actions triggered by the user, such as "Find in Page" or
* opening a specific search item.
* This method sets up observers for navigation results related to search functionality.
*/
private fun observeSearchActions() {
requireActivity().observeNavigationResult<String>(
FIND_IN_PAGE_SEARCH_STRING,
viewLifecycleOwner,
Observer(::findInPage)
)
requireActivity().observeNavigationResult<SearchItemToOpen>(
TAG_FILE_SEARCHED,
viewLifecycleOwner,
Observer(::openSearchItem)
)
} }
override fun onReadAloudPauseOrResume(isPauseTTS: Boolean) { override fun onReadAloudPauseOrResume(isPauseTTS: Boolean) {

View File

@ -90,8 +90,10 @@ class SearchFragmentTestForCustomApp {
private lateinit var downloadingZimFile: File private lateinit var downloadingZimFile: File
private lateinit var activityScenario: ActivityScenario<CustomMainActivity> private lateinit var activityScenario: ActivityScenario<CustomMainActivity>
private val rayCharlesZimFileUrl = private val scientificAllianceZIMUrl =
"https://download.kiwix.org/zim/zimit/scientific-alliance.obscurative.ru_ru_all_2024-06.zim" "https://download.kiwix.org/zim/zimit/scientific-alliance.obscurative.ru_ru_all_2024-06.zim"
private val rayCharlesZIMFileUrl =
"https://dev.kiwix.org/kiwix-android/test/wikipedia_en_ray_charles_maxi_2023-12.zim"
@Before @Before
fun waitForIdle() { fun waitForIdle() {
@ -129,7 +131,7 @@ class SearchFragmentTestForCustomApp {
customMainActivity = it customMainActivity = it
} }
// test with a large ZIM file to properly test the scenario // test with a large ZIM file to properly test the scenario
downloadingZimFile = getDownloadingZimFile() downloadingZimFile = getDownloadingZimFileFromDataFolder()
getOkkHttpClientForTesting().newCall(downloadRequest()).execute().use { response -> getOkkHttpClientForTesting().newCall(downloadRequest()).execute().use { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
response.body?.let { responseBody -> response.body?.let { responseBody ->
@ -145,7 +147,7 @@ class SearchFragmentTestForCustomApp {
UiThreadStatement.runOnUiThread { UiThreadStatement.runOnUiThread {
customMainActivity.navigate(customMainActivity.readerFragmentResId) customMainActivity.navigate(customMainActivity.readerFragmentResId)
} }
openZimFileInReaderWithAssetFileDescriptor(downloadingZimFile) openZimFileInReader(zimFile = downloadingZimFile)
openSearchWithQuery() openSearchWithQuery()
val searchTerm = "gard" val searchTerm = "gard"
val searchedItem = "Gardanta Spirito" val searchedItem = "Gardanta Spirito"
@ -258,6 +260,44 @@ class SearchFragmentTestForCustomApp {
} }
} }
@Test
fun testPreviouslyLoadedArticleLoadsAgainWhenSwitchingToAnotherScreen() {
activityScenario.onActivity {
customMainActivity = it
}
// test with a large ZIM file to properly test the scenario
downloadingZimFile = getDownloadingZimFileFromDataFolder()
getOkkHttpClientForTesting().newCall(downloadRequest(rayCharlesZIMFileUrl)).execute()
.use { response ->
if (response.isSuccessful) {
response.body?.let { responseBody ->
writeZimFileData(responseBody, downloadingZimFile)
}
} else {
throw RuntimeException(
"Download Failed. Error: ${response.message}\n" +
" Status Code: ${response.code}"
)
}
}
UiThreadStatement.runOnUiThread {
customMainActivity.navigate(customMainActivity.readerFragmentResId)
}
openZimFileInReader(zimFile = downloadingZimFile)
search {
// click on home button to load the main page of ZIM file.
clickOnHomeButton()
// click on an article to load the other page.
clickOnAFoolForYouArticle()
assertAFoolForYouArticleLoaded()
// open note screen.
openNoteFragment()
pressBack()
// after came back check the previously loaded article is still showing or not.
assertAFoolForYouArticleLoaded()
}
}
private fun openSearchWithQuery(query: String = "") { private fun openSearchWithQuery(query: String = "") {
UiThreadStatement.runOnUiThread { UiThreadStatement.runOnUiThread {
customMainActivity.openSearch(searchString = query) customMainActivity.openSearch(searchString = query)
@ -270,7 +310,10 @@ class SearchFragmentTestForCustomApp {
} }
} }
private fun openZimFileInReader(assetFileDescriptor: AssetFileDescriptor) { private fun openZimFileInReader(
assetFileDescriptor: AssetFileDescriptor? = null,
zimFile: File? = null
) {
UiThreadStatement.runOnUiThread { UiThreadStatement.runOnUiThread {
val navHostFragment: NavHostFragment = val navHostFragment: NavHostFragment =
customMainActivity.supportFragmentManager customMainActivity.supportFragmentManager
@ -280,10 +323,17 @@ class SearchFragmentTestForCustomApp {
val customReaderFragment = val customReaderFragment =
navHostFragment.childFragmentManager.fragments[0] as CustomReaderFragment navHostFragment.childFragmentManager.fragments[0] as CustomReaderFragment
runBlocking { runBlocking {
customReaderFragment.openZimFile( assetFileDescriptor?.let {
ZimReaderSource(null, null, listOf(assetFileDescriptor)), customReaderFragment.openZimFile(
true ZimReaderSource(assetFileDescriptorList = listOf(assetFileDescriptor)),
) true
)
} ?: run {
customReaderFragment.openZimFile(
ZimReaderSource(zimFile),
true
)
}
} }
} }
} }
@ -318,9 +368,9 @@ class SearchFragmentTestForCustomApp {
} }
} }
private fun downloadRequest() = private fun downloadRequest(zimUrl: String = scientificAllianceZIMUrl) =
Request.Builder() Request.Builder()
.url(URI.create(rayCharlesZimFileUrl).toURL()) .url(URI.create(zimUrl).toURL())
.build() .build()
private fun getDownloadingZimFile(): File { private fun getDownloadingZimFile(): File {
@ -330,6 +380,13 @@ class SearchFragmentTestForCustomApp {
return zimFile return zimFile
} }
private fun getDownloadingZimFileFromDataFolder(): File {
val zimFile = File(context.getExternalFilesDirs(null)[0], "ray_charles.zim")
if (zimFile.exists()) zimFile.delete()
zimFile.createNewFile()
return zimFile
}
@Singleton @Singleton
private fun getOkkHttpClientForTesting(): OkHttpClient = private fun getOkkHttpClientForTesting(): OkHttpClient =
OkHttpClient().newBuilder() OkHttpClient().newBuilder()

View File

@ -19,19 +19,31 @@
package org.kiwix.kiwixmobile.custom.search package org.kiwix.kiwixmobile.custom.search
import android.view.KeyEvent import android.view.KeyEvent
import androidx.core.view.GravityCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.sugar.Web
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms import androidx.test.espresso.web.webdriver.DriverAtoms
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.DriverAtoms.webClick
import androidx.test.espresso.web.webdriver.Locator import androidx.test.espresso.web.webdriver.Locator
import androidx.test.uiautomator.UiDevice 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.interaction.BaristaSleepInteractions
import com.adevinta.android.barista.internal.matcher.HelperMatchers import com.adevinta.android.barista.internal.matcher.HelperMatchers
import org.hamcrest.CoreMatchers.containsString
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.custom.R.id
import org.kiwix.kiwixmobile.custom.testutils.TestUtils import org.kiwix.kiwixmobile.custom.testutils.TestUtils
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.testFlakyView import org.kiwix.kiwixmobile.custom.testutils.TestUtils.testFlakyView
@ -88,14 +100,16 @@ class SearchRobot {
} }
fun searchAndClickOnArticle(searchString: String) { fun searchAndClickOnArticle(searchString: String) {
// wait a bit to properly load the ZIM file in the reader // Wait a bit to properly load the ZIM file in the reader.
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
openSearchScreen() openSearchScreen()
// Wait a bit to properly visible the search screen.
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
searchWithFrequentlyTypedWords(searchString) searchWithFrequentlyTypedWords(searchString)
clickOnSearchItemInSearchList() clickOnSearchItemInSearchList()
} }
fun clickOnSearchItemInSearchList() { private fun clickOnSearchItemInSearchList() {
testFlakyView({ testFlakyView({
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
Espresso.onView(ViewMatchers.withId(R.id.search_list)).perform( Espresso.onView(ViewMatchers.withId(R.id.search_list)).perform(
@ -118,4 +132,43 @@ class SearchRobot {
) )
}) })
} }
fun clickOnHomeButton() {
testFlakyView({
Espresso.onView(ViewMatchers.withId(R.id.bottom_toolbar_home))
.perform(ViewActions.click())
})
}
fun clickOnAFoolForYouArticle() {
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
testFlakyView({
onWebView()
.withElement(
findElement(
Locator.XPATH,
"//*[contains(text(), 'A Fool for You')]"
)
).perform(webClick())
})
}
fun assertAFoolForYouArticleLoaded() {
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
testFlakyView({
onWebView()
.withElement(
findElement(
Locator.XPATH,
"//*[contains(text(), '\"A Fool for You\"')]"
)
).check(webMatches(getText(), containsString("\"A Fool for You\"")))
})
}
fun openNoteFragment() {
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
openDrawerWithGravity(id.custom_drawer_container, GravityCompat.START)
testFlakyView({ onView(withText(R.string.pref_notes)).perform(click()) })
}
} }

View File

@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.custom.testutils
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiObject
@ -32,6 +31,7 @@ import java.io.File
object TestUtils { object TestUtils {
private const val TAG = "TESTUTILS" private const val TAG = "TESTUTILS"
var TEST_PAUSE_MS_FOR_SEARCH_TEST = 1000 var TEST_PAUSE_MS_FOR_SEARCH_TEST = 1000
var TEST_PAUSE_MS = 3000
@JvmStatic @JvmStatic
fun isSystemUINotRespondingDialogVisible(uiDevice: UiDevice) = fun isSystemUINotRespondingDialogVisible(uiDevice: UiDevice) =
@ -92,7 +92,7 @@ object TestUtils {
@JvmStatic @JvmStatic
fun deleteTemporaryFilesOfTestCases(context: Context) { fun deleteTemporaryFilesOfTestCases(context: Context) {
ContextCompat.getExternalFilesDirs(context, null).filterNotNull() context.getExternalFilesDirs(null).filterNotNull()
.map(::deleteAllFilesInDirectory) .map(::deleteAllFilesInDirectory)
ContextWrapper(context).externalMediaDirs.filterNotNull() ContextWrapper(context).externalMediaDirs.filterNotNull()
.map(::deleteAllFilesInDirectory) .map(::deleteAllFilesInDirectory)

View File

@ -144,7 +144,6 @@ class CustomReaderFragment : CoreReaderFragment() {
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks) zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
} else { } else {
openObbOrZim() openObbOrZim()
manageExternalLaunchAndRestoringViewState()
} }
requireArguments().clear() requireArguments().clear()
} }
@ -207,11 +206,15 @@ class CustomReaderFragment : CoreReaderFragment() {
val bookOnDisk = BookOnDisk(zimFileReader) val bookOnDisk = BookOnDisk(zimFileReader)
repositoryActions?.saveBook(bookOnDisk) repositoryActions?.saveBook(bookOnDisk)
} }
// Open the previous loaded pages after ZIM file loads.
manageExternalLaunchAndRestoringViewState()
} }
is ValidationState.HasBothFiles -> { is ValidationState.HasBothFiles -> {
it.zimFile.delete() it.zimFile.delete()
openZimFile(ZimReaderSource(it.obbFile), true) openZimFile(ZimReaderSource(it.obbFile), true)
// Open the previous loaded pages after ZIM file loads.
manageExternalLaunchAndRestoringViewState()
} }
else -> {} else -> {}