mirror of
https://github.com/TeamAmaze/AmazeFileManager.git
synced 2026-10-17 05:08:00 +08:00
Fix Espresso tests failing on #4692
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
package com.amaze.filemanager.test
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
@@ -40,34 +42,37 @@ object StoragePermissionHelper {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun grantManageStoragePermission() {
|
||||
// Ensure that an activity that has the dialog is launched
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
// Only need to run on Androids >= R
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
|
||||
// Ensure that an activity that has the dialog is launched
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
val amazeResources = context.packageManager.getResourcesForApplication(context.packageName)
|
||||
val grantPermissionExplanation = amazeResources.getString(R.string.grant_all_files_permission)
|
||||
val amazeResources = context.packageManager.getResourcesForApplication(context.packageName)
|
||||
val grantPermissionExplanation = amazeResources.getString(R.string.grant_all_files_permission)
|
||||
|
||||
if (device.hasObject(By.text(grantPermissionExplanation))) {
|
||||
// First press Amaze's grant button
|
||||
onView(withText(R.string.grant)).perform(click())
|
||||
if (device.hasObject(By.text(grantPermissionExplanation))) {
|
||||
// First press Amaze's grant button
|
||||
onView(withText(R.string.grant)).perform(click())
|
||||
|
||||
// Identifier names are taken here:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/res/values/strings.xml
|
||||
val resources = context.packageManager.getResourcesForApplication("com.android.settings")
|
||||
val resId =
|
||||
resources.getIdentifier(
|
||||
"permit_manage_external_storage",
|
||||
"string",
|
||||
"com.android.settings",
|
||||
)
|
||||
val permitManageExternalStorage = resources.getString(resId)
|
||||
// Identifier names are taken here:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/res/values/strings.xml
|
||||
val resources = context.packageManager.getResourcesForApplication("com.android.settings")
|
||||
val resId =
|
||||
resources.getIdentifier(
|
||||
"permit_manage_external_storage",
|
||||
"string",
|
||||
"com.android.settings",
|
||||
)
|
||||
val permitManageExternalStorage = resources.getString(resId)
|
||||
|
||||
val grantToggle =
|
||||
device.findObject(UiSelector().textMatches("(?i)$permitManageExternalStorage"))
|
||||
grantToggle.click()
|
||||
device.pressBack()
|
||||
val grantToggle =
|
||||
device.findObject(UiSelector().textMatches("(?i)$permitManageExternalStorage"))
|
||||
grantToggle.click()
|
||||
device.pressBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-39
@@ -27,13 +27,13 @@ import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.TIRAMISU
|
||||
import android.os.Environment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
@@ -43,6 +43,7 @@ import com.amaze.filemanager.ui.activities.PreferencesActivity
|
||||
import com.amaze.filemanager.ui.fragments.preferencefragments.BackupPrefsFragment
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import org.awaitility.Awaitility.await
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -52,10 +53,11 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BackupPrefsFragmentTest {
|
||||
var storagePath = "/storage/emulated/0"
|
||||
var storagePath = Environment.getExternalStorageDirectory().absolutePath
|
||||
var fileName = "amaze_backup.json"
|
||||
|
||||
@Rule
|
||||
@@ -93,6 +95,19 @@ class BackupPrefsFragmentTest {
|
||||
import(exportFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits (with a timeout) for the given file to exist, since some writes to storage happen
|
||||
* asynchronously on a background thread.
|
||||
*/
|
||||
private fun waitForFile(
|
||||
file: File,
|
||||
timeoutSeconds: Long = 5L,
|
||||
) {
|
||||
await().atMost(timeoutSeconds, TimeUnit.SECONDS).until {
|
||||
file.exists()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the exported file contains the expected preference values
|
||||
*/
|
||||
@@ -103,59 +118,70 @@ class BackupPrefsFragmentTest {
|
||||
val backupPrefsFragment = BackupPrefsFragment()
|
||||
val activityScenario = ActivityScenario.launch(PreferencesActivity::class.java)
|
||||
|
||||
activityScenario.moveToState(Lifecycle.State.STARTED)
|
||||
// Espresso requires an activity to be RESUMED to dispatch view actions/clicks.
|
||||
activityScenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
activityScenario.onActivity {
|
||||
it.supportFragmentManager.beginTransaction()
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
activityScenario.onActivity { preferencesActivity ->
|
||||
preferencesActivity.supportFragmentManager.beginTransaction()
|
||||
.add(backupPrefsFragment, null)
|
||||
.commitNow()
|
||||
|
||||
backupPrefsFragment.exportPrefs()
|
||||
|
||||
val tempFile = File("${context.cacheDir.absolutePath}${File.separator}$fileName")
|
||||
|
||||
assertTrue(tempFile.exists())
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(preferencesActivity)
|
||||
}
|
||||
|
||||
val tempFile = File("${context.cacheDir.absolutePath}${File.separator}$fileName")
|
||||
|
||||
assertTrue(tempFile.exists())
|
||||
|
||||
onView(withId(R.id.home)).perform(ViewActions.click())
|
||||
// Espresso's onView().perform() must run on the instrumentation/test thread, never from
|
||||
// inside onActivity {} or runOnUiThread {} (both of which run on the main/UI thread).
|
||||
// Espresso internally synchronizes with the UI thread itself; calling it from the UI
|
||||
// thread can deadlock or throw IllegalStateException.
|
||||
// exportPrefs() launches MainActivity with an ACTION_SEND intent, which shows a Snackbar
|
||||
// with a "Save" action; that is the only view action needed here.
|
||||
onView(withText(R.string.save)).perform(ViewActions.click())
|
||||
|
||||
assertTrue(exportFile.exists())
|
||||
// The actual write to storagePath happens asynchronously (RxJava) after the "Save" click
|
||||
// and after MainActivity finishes, so poll for the file instead of asserting immediately.
|
||||
waitForFile(exportFile)
|
||||
|
||||
activityScenario.onActivity { preferencesActivity ->
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(preferencesActivity)
|
||||
val preferenceMap: Map<String?, *> = preferences.all
|
||||
val preferenceMap: Map<String?, *> = preferences.all
|
||||
|
||||
val inputString =
|
||||
exportFile
|
||||
.inputStream()
|
||||
.bufferedReader()
|
||||
.use {
|
||||
it.readText()
|
||||
}
|
||||
val inputString =
|
||||
exportFile
|
||||
.inputStream()
|
||||
.bufferedReader()
|
||||
.use {
|
||||
it.readText()
|
||||
}
|
||||
|
||||
val type = object : TypeToken<Map<String?, *>>() {}.type
|
||||
val type = object : TypeToken<Map<String?, *>>() {}.type
|
||||
|
||||
val importMap: Map<String?, *> =
|
||||
GsonBuilder()
|
||||
.create()
|
||||
.fromJson(
|
||||
inputString,
|
||||
type,
|
||||
)
|
||||
val importMap: Map<String?, *> =
|
||||
GsonBuilder()
|
||||
.create()
|
||||
.fromJson(
|
||||
inputString,
|
||||
type,
|
||||
)
|
||||
|
||||
for ((key, value) in preferenceMap) {
|
||||
val importedValue = importMap[key]
|
||||
val mapValue =
|
||||
if (importedValue != null && importedValue::class.simpleName.equals("Double")) {
|
||||
(importedValue as Double).toInt() // since Gson parses Integer as Double
|
||||
} else {
|
||||
importedValue
|
||||
}
|
||||
for ((key, value) in preferenceMap) {
|
||||
val importedValue = importMap[key]
|
||||
val mapValue =
|
||||
if (importedValue != null && importedValue::class.simpleName.equals("Double")) {
|
||||
(importedValue as Double).toInt() // since Gson parses Integer as Double
|
||||
} else {
|
||||
importedValue
|
||||
}
|
||||
|
||||
assertEquals("Difference found at key $key", value, mapValue)
|
||||
}
|
||||
assertEquals("Difference found at key $key", value, mapValue)
|
||||
}
|
||||
|
||||
activityScenario.close()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,6 +237,8 @@ class BackupPrefsFragmentTest {
|
||||
assertTrue("checkPrefEqual($key) failed", checkPrefEqual(preferences, importMap, key, value))
|
||||
}
|
||||
}
|
||||
|
||||
activityScenario.close()
|
||||
}
|
||||
|
||||
private fun checkPrefEqual(
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
package com.amaze.filemanager.ui.fragments
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.TIRAMISU
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.swipeLeft
|
||||
import androidx.test.espresso.action.ViewActions.swipeRight
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.amaze.filemanager.R
|
||||
import com.amaze.filemanager.test.StoragePermissionHelper
|
||||
import com.amaze.filemanager.ui.activities.MainActivity
|
||||
import org.awaitility.Awaitility.await
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Tests for [TabFragment] functionality, mainly for
|
||||
@@ -28,9 +25,6 @@ import org.junit.runner.RunWith
|
||||
@Suppress("DEPRECATION")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TabFragmentTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityTestRule(MainActivity::class.java)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val storagePermissionRule: GrantPermissionRule =
|
||||
@@ -52,25 +46,25 @@ class TabFragmentTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* This test causes a rotation to happen while the MainFragment detaches, to check if it
|
||||
* fails. This could happen in reality, but should be very rare
|
||||
* This test saves state while a MainFragment is detached.
|
||||
*/
|
||||
@Test
|
||||
fun testFragmentStateSavingDuringDetachment() {
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
withScenario { scenario ->
|
||||
awaitTabFragment(scenario)
|
||||
|
||||
// Get the TabFragment
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
val activity = activityRule.activity
|
||||
val tabFragment =
|
||||
activity.supportFragmentManager
|
||||
.findFragmentById(R.id.content_frame) as TabFragment
|
||||
scenario.onActivity { activity ->
|
||||
val tabFragment =
|
||||
activity.supportFragmentManager
|
||||
.findFragmentById(R.id.content_frame) as TabFragment
|
||||
|
||||
// Detach fragment through FragmentManager
|
||||
activity.supportFragmentManager.beginTransaction().apply {
|
||||
tabFragment.fragments.forEach { detach(it) }
|
||||
commit()
|
||||
activity.supportFragmentManager.beginTransaction().apply {
|
||||
tabFragment.fragments.firstOrNull { it.isAdded }?.let { detach(it) }
|
||||
commitNow()
|
||||
}
|
||||
}
|
||||
|
||||
recreateActivity(scenario)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +74,11 @@ class TabFragmentTest {
|
||||
*/
|
||||
@Test
|
||||
fun testFragmentStateSavingDuringConfigChange() {
|
||||
// First perform the swipe action
|
||||
onView(withId(R.id.pager)).perform(swipeLeft())
|
||||
|
||||
// Force a configuration change by rotating the screen
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
Thread.sleep(1000) // Give time for the rotation to complete
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
withScenario { scenario ->
|
||||
setCurrentItem(scenario, 1)
|
||||
recreateActivity(scenario)
|
||||
awaitCurrentItem(scenario, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,16 +86,15 @@ class TabFragmentTest {
|
||||
*/
|
||||
@Test
|
||||
fun testRapidTabSwitchingAndStateSaving() {
|
||||
// Perform rapid tab switches
|
||||
repeat(10) {
|
||||
onView(withId(R.id.pager)).perform(swipeLeft())
|
||||
Thread.sleep(100) // Small delay to ensure swipe completes
|
||||
onView(withId(R.id.pager)).perform(swipeRight())
|
||||
Thread.sleep(100) // Small delay to ensure swipe completes
|
||||
}
|
||||
withScenario { scenario ->
|
||||
repeat(10) {
|
||||
setCurrentItem(scenario, 1)
|
||||
setCurrentItem(scenario, 0)
|
||||
}
|
||||
|
||||
// Force a save state by rotating
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
recreateActivity(scenario)
|
||||
awaitCurrentItem(scenario, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,24 +102,97 @@ class TabFragmentTest {
|
||||
*/
|
||||
@Test
|
||||
fun testFragmentDetachmentAndStateSaving() {
|
||||
// First switch to a different tab
|
||||
onView(withId(R.id.pager)).perform(swipeLeft())
|
||||
withScenario { scenario ->
|
||||
setCurrentItem(scenario, 1)
|
||||
awaitTabFragment(scenario)
|
||||
|
||||
// Get the TabFragment
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
val activity = activityRule.activity
|
||||
val tabFragment =
|
||||
activity.supportFragmentManager
|
||||
.findFragmentById(R.id.content_frame) as TabFragment
|
||||
scenario.onActivity { activity ->
|
||||
val tabFragment =
|
||||
activity.supportFragmentManager
|
||||
.findFragmentById(R.id.content_frame) as TabFragment
|
||||
|
||||
// Detach fragment through FragmentManager
|
||||
activity.supportFragmentManager.beginTransaction().apply {
|
||||
tabFragment.fragments.firstOrNull()?.let { detach(it) }
|
||||
commit()
|
||||
activity.supportFragmentManager.beginTransaction().apply {
|
||||
tabFragment.fragments.firstOrNull { it.isAdded }?.let { detach(it) }
|
||||
commitNow()
|
||||
}
|
||||
}
|
||||
|
||||
recreateActivity(scenario)
|
||||
}
|
||||
}
|
||||
|
||||
private fun withScenario(testBody: (ActivityScenario<MainActivity>) -> Unit) {
|
||||
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
||||
awaitPager(scenario)
|
||||
testBody(scenario)
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitPager(scenario: ActivityScenario<MainActivity>): ViewPager2 {
|
||||
var pager: ViewPager2? = null
|
||||
|
||||
await().atMost(10, TimeUnit.SECONDS).until {
|
||||
scenario.onActivity { activity ->
|
||||
pager = activity.findViewById(R.id.pager)
|
||||
}
|
||||
|
||||
pager != null
|
||||
}
|
||||
|
||||
// Force state save through configuration change
|
||||
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
return requireNotNull(pager)
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitTabFragment(scenario: ActivityScenario<MainActivity>): TabFragment {
|
||||
var tabFragment: TabFragment? = null
|
||||
|
||||
await().atMost(10, TimeUnit.SECONDS).until {
|
||||
runCatching {
|
||||
scenario.onActivity { activity ->
|
||||
tabFragment =
|
||||
activity.supportFragmentManager
|
||||
.findFragmentById(R.id.content_frame) as? TabFragment
|
||||
}
|
||||
}
|
||||
|
||||
tabFragment?.view != null && tabFragment?.fragments?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
return requireNotNull(tabFragment)
|
||||
}
|
||||
|
||||
private fun setCurrentItem(
|
||||
scenario: ActivityScenario<MainActivity>,
|
||||
index: Int,
|
||||
) {
|
||||
awaitPager(scenario)
|
||||
|
||||
scenario.onActivity { activity ->
|
||||
activity.findViewById<ViewPager2>(R.id.pager).setCurrentItem(index, false)
|
||||
}
|
||||
|
||||
awaitCurrentItem(scenario, index)
|
||||
}
|
||||
|
||||
private fun awaitCurrentItem(
|
||||
scenario: ActivityScenario<MainActivity>,
|
||||
index: Int,
|
||||
) {
|
||||
await().atMost(5, TimeUnit.SECONDS).until {
|
||||
var currentItem = -1
|
||||
|
||||
runCatching {
|
||||
scenario.onActivity { activity ->
|
||||
currentItem = activity.findViewById<ViewPager2>(R.id.pager).currentItem
|
||||
}
|
||||
}
|
||||
|
||||
currentItem == index
|
||||
}
|
||||
}
|
||||
|
||||
private fun recreateActivity(scenario: ActivityScenario<MainActivity>) {
|
||||
scenario.recreate()
|
||||
awaitPager(scenario)
|
||||
awaitTabFragment(scenario)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user