mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-08 23:07:26 -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
|
channel: canary
|
||||||
script: bash contrib/instrumentation.sh
|
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
|
- name: Upload screenshot result
|
||||||
uses: actions/upload-artifact@v3
|
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)
|
unsupportedMimeTypeHandler?.showSaveOrOpenUnsupportedFilesDialog(url, documentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun openZimFile(
|
fun openZimFile(
|
||||||
file: File?,
|
file: File?,
|
||||||
isCustomApp: Boolean = false,
|
isCustomApp: Boolean = false,
|
||||||
assetFileDescriptorList: List<AssetFileDescriptor> = emptyList(),
|
assetFileDescriptorList: List<AssetFileDescriptor> = emptyList(),
|
||||||
|
@ -124,7 +124,7 @@ class ZimFileReader constructor(
|
|||||||
): Array<FdInput> =
|
): Array<FdInput> =
|
||||||
assetFileDescriptorList.map {
|
assetFileDescriptorList.map {
|
||||||
FdInput(
|
FdInput(
|
||||||
it.parcelFileDescriptor.fileDescriptor,
|
it.parcelFileDescriptor.dup().fileDescriptor,
|
||||||
it.startOffset,
|
it.startOffset,
|
||||||
it.length
|
it.length
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ class ZimReaderContainer @Inject constructor(private val zimFileReaderFactory: F
|
|||||||
) {
|
) {
|
||||||
zimFileReader = runBlocking {
|
zimFileReader = runBlocking {
|
||||||
if (assetFileDescriptorList.isNotEmpty() &&
|
if (assetFileDescriptorList.isNotEmpty() &&
|
||||||
assetFileDescriptorList[0].parcelFileDescriptor.fileDescriptor.valid()
|
assetFileDescriptorList[0].parcelFileDescriptor.dup().fileDescriptor.valid()
|
||||||
)
|
)
|
||||||
zimFileReaderFactory.create(assetFileDescriptorList, filePath)
|
zimFileReaderFactory.create(assetFileDescriptorList, filePath)
|
||||||
else null
|
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 =
|
override val topLevelDestinations =
|
||||||
setOf(R.id.customReaderFragment)
|
setOf(R.id.customReaderFragment)
|
||||||
|
|
||||||
private lateinit var activityCustomMainBinding: ActivityCustomMainBinding
|
lateinit var activityCustomMainBinding: ActivityCustomMainBinding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
customActivityComponent.inject(this)
|
customActivityComponent.inject(this)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user