mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Added test cases for custom apps so that we can avoid the error in search functionality of custom apps.
This commit is contained in:
parent
16844541c0
commit
2a4c9c51d4
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@ -89,6 +89,25 @@ jobs:
|
||||
channel: canary
|
||||
script: bash contrib/instrumentation.sh
|
||||
|
||||
- name: Test custom app
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
env:
|
||||
GRADLE_OPTS: "-Dorg.gradle.internal.http.connectionTimeout=60000 -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.network.retry.max.attempts=6 -Dorg.gradle.internal.network.retry.initial.backOff=2000"
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.api-level != 33 && 'default' || 'aosp_atd' }}
|
||||
arch: x86_64
|
||||
profile: pixel_2
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
ram-size: 4096M
|
||||
cores: 4
|
||||
force-avd-creation: false
|
||||
sdcard-path-or-size: 2048M
|
||||
disable-animations: true
|
||||
heap-size: 512M
|
||||
channel: canary
|
||||
script: bash contrib/instrumentation-customapps.sh
|
||||
|
||||
|
||||
- name: Upload screenshot result
|
||||
uses: actions/upload-artifact@v3
|
||||
|
72
contrib/instrumentation-customapps.sh
Normal file
72
contrib/instrumentation-customapps.sh
Normal file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Kiwix Android
|
||||
# Copyright (c) 2024 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/>.
|
||||
#
|
||||
#
|
||||
|
||||
# Enable Wi-Fi on the emulator
|
||||
adb shell svc wifi enable
|
||||
adb logcat -c
|
||||
# shellcheck disable=SC2035
|
||||
adb logcat *:E -v color &
|
||||
|
||||
PACKAGE_NAME="org.kiwix.kiwixmobile.custom"
|
||||
TEST_PACKAGE_NAME="${PACKAGE_NAME}.test"
|
||||
# Function to check if the application is installed
|
||||
is_app_installed() {
|
||||
adb shell pm list packages | grep -q "$1"
|
||||
}
|
||||
|
||||
if is_app_installed "$PACKAGE_NAME"; then
|
||||
# Delete the application to properly run the test cases.
|
||||
adb uninstall "${PACKAGE_NAME}"
|
||||
fi
|
||||
|
||||
if is_app_installed "$TEST_PACKAGE_NAME"; then
|
||||
# Delete the test application to properly run the test cases.
|
||||
adb uninstall "${TEST_PACKAGE_NAME}"
|
||||
fi
|
||||
retry=0
|
||||
while [ $retry -le 3 ]; do
|
||||
if ./gradlew connectedCustomexampleDebugAndroidTest; then
|
||||
echo "connectedCustomexampleDebugAndroidTest succeeded" >&2
|
||||
break
|
||||
else
|
||||
adb kill-server
|
||||
adb start-server
|
||||
# Enable Wi-Fi on the emulator
|
||||
adb shell svc wifi enable
|
||||
adb logcat -c
|
||||
# shellcheck disable=SC2035
|
||||
adb logcat *:E -v color &
|
||||
|
||||
if is_app_installed "$PACKAGE_NAME"; then
|
||||
# Delete the application to properly run the test cases.
|
||||
adb uninstall "${PACKAGE_NAME}"
|
||||
fi
|
||||
if is_app_installed "$TEST_PACKAGE_NAME"; then
|
||||
# Delete the test application to properly run the test cases.
|
||||
adb uninstall "${TEST_PACKAGE_NAME}"
|
||||
fi
|
||||
./gradlew clean
|
||||
retry=$(( retry + 1 ))
|
||||
if [ $retry -eq 3 ]; then
|
||||
adb exec-out screencap -p >screencap.png
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
@ -1551,7 +1551,7 @@ abstract class CoreReaderFragment :
|
||||
unsupportedMimeTypeHandler?.showSaveOrOpenUnsupportedFilesDialog(url, documentType)
|
||||
}
|
||||
|
||||
protected fun openZimFile(
|
||||
fun openZimFile(
|
||||
file: File?,
|
||||
isCustomApp: Boolean = false,
|
||||
assetFileDescriptorList: List<AssetFileDescriptor> = emptyList(),
|
||||
|
@ -124,7 +124,7 @@ class ZimFileReader constructor(
|
||||
): Array<FdInput> =
|
||||
assetFileDescriptorList.map {
|
||||
FdInput(
|
||||
it.parcelFileDescriptor.fileDescriptor,
|
||||
it.parcelFileDescriptor.dup().fileDescriptor,
|
||||
it.startOffset,
|
||||
it.length
|
||||
)
|
||||
|
@ -51,7 +51,7 @@ class ZimReaderContainer @Inject constructor(private val zimFileReaderFactory: F
|
||||
) {
|
||||
zimFileReader = runBlocking {
|
||||
if (assetFileDescriptorList.isNotEmpty() &&
|
||||
assetFileDescriptorList[0].parcelFileDescriptor.fileDescriptor.valid()
|
||||
assetFileDescriptorList[0].parcelFileDescriptor.dup().fileDescriptor.valid()
|
||||
)
|
||||
zimFileReaderFactory.create(assetFileDescriptorList, filePath)
|
||||
else null
|
||||
|
30
custom/src/androidTest/AndroidManifest.xml
Normal file
30
custom/src/androidTest/AndroidManifest.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Kiwix Android
|
||||
~ Copyright (c) 2024 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/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.kiwix.kiwixmobile.custom">
|
||||
|
||||
<!-- support for androidx.test.orchestrator clearPackageData for android 33
|
||||
see more information here https://github.com/kiwix/kiwix-android/issues/3172
|
||||
-->
|
||||
<application
|
||||
android:forceQueryable="true"
|
||||
android:usesCleartextTraffic="true" />
|
||||
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator" />
|
||||
</manifest>
|
@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2024 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.custom.search
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.res.AssetFileDescriptor
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
import androidx.test.espresso.accessibility.AccessibilityChecks
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
|
||||
import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
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.custom.main.CustomMainActivity
|
||||
import org.kiwix.kiwixmobile.custom.main.CustomReaderFragment
|
||||
import org.kiwix.kiwixmobile.custom.testutils.RetryRule
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.closeSystemDialogs
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.isSystemUINotRespondingDialogVisible
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SearchFragmentTestForCustomApp {
|
||||
private val permissions = arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
@get:Rule
|
||||
var permissionRules: GrantPermissionRule =
|
||||
GrantPermissionRule.grant(*permissions)
|
||||
|
||||
private val context: Context by lazy {
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
var retryRule = RetryRule()
|
||||
|
||||
private lateinit var customMainActivity: CustomMainActivity
|
||||
private lateinit var uiDevice: UiDevice
|
||||
private lateinit var downloadingZimFile: File
|
||||
private lateinit var activityScenario: ActivityScenario<CustomMainActivity>
|
||||
|
||||
private val rayCharlesZimFileUrl =
|
||||
"https://dev.kiwix.org/kiwix-android/test/wikipedia_en_ray_charles_maxi_2023-12.zim"
|
||||
|
||||
@Before
|
||||
fun waitForIdle() {
|
||||
uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply {
|
||||
if (isSystemUINotRespondingDialogVisible(this)) {
|
||||
closeSystemDialogs(context, this)
|
||||
}
|
||||
waitForIdle()
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(SharedPreferenceUtil.PREF_SHOW_INTRO, false)
|
||||
putBoolean(SharedPreferenceUtil.PREF_WIFI_ONLY, false)
|
||||
putBoolean(SharedPreferenceUtil.PREF_IS_TEST, true)
|
||||
putBoolean(SharedPreferenceUtil.PREF_PLAY_STORE_RESTRICTION, false)
|
||||
putString(SharedPreferenceUtil.PREF_LANG, "en")
|
||||
}
|
||||
activityScenario = ActivityScenario.launch(CustomMainActivity::class.java).apply {
|
||||
moveToState(Lifecycle.State.RESUMED)
|
||||
onActivity {
|
||||
LanguageUtils.handleLocaleChange(
|
||||
it,
|
||||
"en",
|
||||
SharedPreferenceUtil(context)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
AccessibilityChecks.enable().apply {
|
||||
setRunChecksFromRootView(true)
|
||||
setSuppressingResultMatcher(
|
||||
Matchers.allOf(
|
||||
AccessibilityCheckResultUtils.matchesCheck(TouchTargetSizeCheck::class.java),
|
||||
AccessibilityCheckResultUtils.matchesViews(
|
||||
ViewMatchers.withId(org.kiwix.kiwixmobile.core.R.id.menu_searchintext)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchFragment() {
|
||||
activityScenario.onActivity {
|
||||
customMainActivity = it
|
||||
}
|
||||
// test with a large ZIM file to properly test the scenario
|
||||
downloadingZimFile = getDownloadingZimFile()
|
||||
if (downloadingZimFile.length() == 0L) {
|
||||
OkHttpClient().newCall(downloadRequest()).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}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
openZimFileInReaderWithAssetFileDescriptor(downloadingZimFile)
|
||||
openSearchWithQuery()
|
||||
val searchTerm = "A Fool"
|
||||
val searchedItem = "A Fool for You"
|
||||
search {
|
||||
// test with fast typing/deleting
|
||||
searchWithFrequentlyTypedWords(searchTerm)
|
||||
assertSearchSuccessful(searchedItem)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
// to close the keyboard
|
||||
pressBack()
|
||||
// go to reader screen
|
||||
pressBack()
|
||||
}
|
||||
|
||||
// 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(searchTerm)
|
||||
searchAndClickOnArticle("A Song")
|
||||
searchAndClickOnArticle("The Ra")
|
||||
searchAndClickOnArticle("The Ge")
|
||||
searchAndClickOnArticle("Wish")
|
||||
searchAndClickOnArticle("WIFI")
|
||||
searchAndClickOnArticle("Woman")
|
||||
searchAndClickOnArticle("Big Ba")
|
||||
searchAndClickOnArticle("My Wor")
|
||||
searchAndClickOnArticle("100")
|
||||
assertArticleLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConcurrencyOfSearch() = runBlocking {
|
||||
val searchTerms = listOf(
|
||||
"A Song",
|
||||
"The Ra",
|
||||
"The Ge",
|
||||
"Wish",
|
||||
"WIFI",
|
||||
"Woman",
|
||||
"Big Ba",
|
||||
"My Wor",
|
||||
"100"
|
||||
)
|
||||
activityScenario.onActivity {
|
||||
customMainActivity = it
|
||||
}
|
||||
// test with a large ZIM file to properly test the scenario
|
||||
downloadingZimFile = getDownloadingZimFile()
|
||||
if (downloadingZimFile.length() == 0L) {
|
||||
OkHttpClient().newCall(downloadRequest()).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}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
openZimFileInReaderWithAssetFileDescriptor(downloadingZimFile)
|
||||
openSearchWithQuery(searchTerms[0])
|
||||
// wait for searchFragment become visible on screen.
|
||||
delay(2000)
|
||||
val navHostFragment: NavHostFragment =
|
||||
customMainActivity.supportFragmentManager
|
||||
.findFragmentById(
|
||||
customMainActivity.activityCustomMainBinding.customNavController.id
|
||||
) as NavHostFragment
|
||||
val searchFragment = navHostFragment.childFragmentManager.fragments[0] as SearchFragment
|
||||
for (i in 1..100) {
|
||||
// This will execute the render method 100 times frequently.
|
||||
val searchTerm = searchTerms[i % searchTerms.size]
|
||||
searchFragment.searchViewModel.actions.trySend(Action.Filter(searchTerm)).isSuccess
|
||||
}
|
||||
for (i in 1..100) {
|
||||
// this will execute the render method 100 times with 100MS delay.
|
||||
delay(100)
|
||||
val searchTerm = searchTerms[i % searchTerms.size]
|
||||
searchFragment.searchViewModel.actions.trySend(Action.Filter(searchTerm)).isSuccess
|
||||
}
|
||||
for (i in 1..100) {
|
||||
// this will execute the render method 100 times with 200MS delay.
|
||||
delay(200)
|
||||
val searchTerm = searchTerms[i % searchTerms.size]
|
||||
searchFragment.searchViewModel.actions.trySend(Action.Filter(searchTerm)).isSuccess
|
||||
}
|
||||
for (i in 1..100) {
|
||||
// this will execute the render method 100 times with 200MS delay.
|
||||
delay(300)
|
||||
val searchTerm = searchTerms[i % searchTerms.size]
|
||||
searchFragment.searchViewModel.actions.trySend(Action.Filter(searchTerm)).isSuccess
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSearchWithQuery(query: String = "") {
|
||||
UiThreadStatement.runOnUiThread {
|
||||
customMainActivity.openSearch(searchString = query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openZimFileInReaderWithAssetFileDescriptor(downloadingZimFile: File) {
|
||||
getAssetFileDescriptorFromFile(downloadingZimFile)?.let(::openZimFileInReader) ?: run {
|
||||
throw RuntimeException("Unable to get fileDescriptor from file. Original exception")
|
||||
}
|
||||
}
|
||||
|
||||
private fun openZimFileInReader(assetFileDescriptor: AssetFileDescriptor) {
|
||||
UiThreadStatement.runOnUiThread {
|
||||
val navHostFragment: NavHostFragment =
|
||||
customMainActivity.supportFragmentManager
|
||||
.findFragmentById(
|
||||
customMainActivity.activityCustomMainBinding.customNavController.id
|
||||
) as NavHostFragment
|
||||
val customReaderFragment =
|
||||
navHostFragment.childFragmentManager.fragments[0] as CustomReaderFragment
|
||||
customReaderFragment.openZimFile(null, true, listOf(assetFileDescriptor))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAssetFileDescriptorFromFile(file: File): AssetFileDescriptor? {
|
||||
val parcelFileDescriptor = getFileDescriptor(file)
|
||||
if (parcelFileDescriptor != null) {
|
||||
return AssetFileDescriptor(parcelFileDescriptor, 0, file.length())
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getFileDescriptor(file: File?): ParcelFileDescriptor? {
|
||||
try {
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeZimFileData(responseBody: ResponseBody, file: File) {
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
responseBody.byteStream().use { inputStream ->
|
||||
val buffer = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
outputStream.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadRequest() =
|
||||
Request.Builder()
|
||||
.url(URI.create(rayCharlesZimFileUrl).toURL())
|
||||
.build()
|
||||
|
||||
private fun getDownloadingZimFile(isDeletePreviousZimFile: Boolean = true): File {
|
||||
val zimFile = File(context.cacheDir, "ray_charles.zim")
|
||||
if (isDeletePreviousZimFile) {
|
||||
if (zimFile.exists()) zimFile.delete()
|
||||
zimFile.createNewFile()
|
||||
}
|
||||
return zimFile
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2024 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.custom.search
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.web.sugar.Web
|
||||
import androidx.test.espresso.web.webdriver.DriverAtoms
|
||||
import androidx.test.espresso.web.webdriver.Locator
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
||||
import com.adevinta.android.barista.internal.matcher.HelperMatchers
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.testFlakyView
|
||||
|
||||
fun search(searchRobot: SearchRobot.() -> Unit) = SearchRobot().searchRobot()
|
||||
|
||||
class SearchRobot {
|
||||
fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) {
|
||||
testFlakyView({
|
||||
val searchView = Espresso.onView(ViewMatchers.withId(R.id.search_src_text))
|
||||
for (char in query) {
|
||||
searchView.perform(ViewActions.typeText(char.toString()))
|
||||
if (wait != 0L) {
|
||||
BaristaSleepInteractions.sleep(wait)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun assertSearchSuccessful(searchResult: String) {
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
val recyclerViewId = R.id.search_list
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(recyclerViewId)).check(
|
||||
ViewAssertions.matches(
|
||||
HelperMatchers.atPosition(
|
||||
0,
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withText(searchResult))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) {
|
||||
for (i in textToDelete.indices) {
|
||||
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(R.id.search_src_text))
|
||||
searchView.perform(ViewActions.clearText())
|
||||
}
|
||||
|
||||
private fun openSearchScreen() {
|
||||
testFlakyView({
|
||||
Espresso.onView(ViewMatchers.withId(R.id.menu_search))
|
||||
.perform(ViewActions.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())
|
||||
openSearchScreen()
|
||||
searchWithFrequentlyTypedWords(searchString)
|
||||
clickOnSearchItemInSearchList()
|
||||
}
|
||||
|
||||
fun clickOnSearchItemInSearchList() {
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun assertArticleLoaded() {
|
||||
testFlakyView({
|
||||
Web.onWebView()
|
||||
.withElement(
|
||||
DriverAtoms.findElement(
|
||||
Locator.XPATH,
|
||||
"//*[contains(text(), 'Big Baby DRAM')]"
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2022 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.custom.testutils
|
||||
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.util.Objects
|
||||
|
||||
class RetryRule : TestRule {
|
||||
val retryCountForFlakyTest = 3
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
statement(base, description)
|
||||
|
||||
private fun statement(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class) override fun evaluate() {
|
||||
var caughtThrowable: Throwable? = null
|
||||
for (i in 0 until retryCountForFlakyTest) {
|
||||
try {
|
||||
base.evaluate()
|
||||
return
|
||||
} catch (t: Throwable) {
|
||||
caughtThrowable = t
|
||||
System.err.println(description.displayName + ": run " + (i + 1) + " failed.")
|
||||
}
|
||||
}
|
||||
System.err.println(
|
||||
description.displayName + ": Giving up after " +
|
||||
retryCountForFlakyTest + " failures."
|
||||
)
|
||||
throw Objects.requireNonNull(caughtThrowable!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2024 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.custom.testutils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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
|
||||
|
||||
object TestUtils {
|
||||
private const val TAG = "TESTUTILS"
|
||||
var TEST_PAUSE_MS_FOR_SEARCH_TEST = 1000
|
||||
|
||||
@JvmStatic
|
||||
fun isSystemUINotRespondingDialogVisible(uiDevice: UiDevice) =
|
||||
uiDevice.findObject(By.textContains("System UI isn't responding")) != null ||
|
||||
uiDevice.findObject(By.textContains("Process system isn't responding")) != null ||
|
||||
uiDevice.findObject(By.textContains("Launcher isn't responding")) != null ||
|
||||
uiDevice.findObject(By.textContains("Wait")) != null ||
|
||||
uiDevice.findObject(By.textContains("WAIT")) != null ||
|
||||
uiDevice.findObject(By.textContains("OK")) != null ||
|
||||
uiDevice.findObject(By.textContains("Ok")) != null ||
|
||||
uiDevice.findObject(By.clazz("android.app.Dialog")) != null
|
||||
|
||||
@JvmStatic
|
||||
fun closeSystemDialogs(context: Context?, uiDevice: UiDevice) {
|
||||
// Close any system dialogs visible on Android versions below 12 by broadcasting
|
||||
context?.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
// Press the back button as most dialogs can be closed by doing so
|
||||
uiDevice.pressBack()
|
||||
try {
|
||||
// Click on the button of system dialog (Especially applicable to non-closable dialogs)
|
||||
val waitButton = getSystemDialogButton(uiDevice)
|
||||
if (waitButton?.exists() == true) {
|
||||
uiDevice.click(waitButton.bounds.centerX(), waitButton.bounds.centerY())
|
||||
}
|
||||
} catch (ignore: Exception) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Couldn't click on Wait/OK button, probably no system dialog is " +
|
||||
"visible with Wait/OK button \n$ignore"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSystemDialogButton(uiDevice: UiDevice): UiObject? {
|
||||
// All possible button text based on different Android versions.
|
||||
val possibleButtonTextList = arrayOf("Wait", "WAIT", "OK", "Ok")
|
||||
return possibleButtonTextList
|
||||
.asSequence()
|
||||
.map { uiDevice.findObject(UiSelector().textContains(it)) }
|
||||
.firstOrNull(UiObject::exists)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun testFlakyView(
|
||||
action: () -> Unit,
|
||||
retryCount: Int = 5
|
||||
) {
|
||||
try {
|
||||
action()
|
||||
} catch (ignore: Throwable) {
|
||||
if (retryCount > 0) {
|
||||
testFlakyView(action, retryCount - 1)
|
||||
} else {
|
||||
throw ignore // No more retries, rethrow the exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ class CustomMainActivity : CoreMainActivity() {
|
||||
override val topLevelDestinations =
|
||||
setOf(R.id.customReaderFragment)
|
||||
|
||||
private lateinit var activityCustomMainBinding: ActivityCustomMainBinding
|
||||
lateinit var activityCustomMainBinding: ActivityCustomMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
customActivityComponent.inject(this)
|
||||
|
Loading…
x
Reference in New Issue
Block a user