Added test cases for custom apps so that we can avoid the error in search functionality of custom apps.

This commit is contained in:
MohitMaliFtechiz 2024-07-03 16:20:37 +05:30
parent 16844541c0
commit 2a4c9c51d4
11 changed files with 716 additions and 4 deletions

View File

@ -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

View 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

View File

@ -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(),

View File

@ -124,7 +124,7 @@ class ZimFileReader constructor(
): Array<FdInput> =
assetFileDescriptorList.map {
FdInput(
it.parcelFileDescriptor.fileDescriptor,
it.parcelFileDescriptor.dup().fileDescriptor,
it.startOffset,
it.length
)

View File

@ -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

View 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>

View File

@ -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
}
}

View File

@ -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')]"
)
)
})
}
}

View File

@ -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!!)
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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)