Skip to content

Commit 305019e

Browse files
committed
Fix Espresso tests failing on #4692
1 parent 1cd239e commit 305019e

2 files changed

Lines changed: 197 additions & 103 deletions

File tree

app/src/androidTest/java/com/amaze/filemanager/ui/fragments/BackupPrefsFragmentTest.kt

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ import android.content.SharedPreferences
2727
import android.net.Uri
2828
import android.os.Build.VERSION.SDK_INT
2929
import android.os.Build.VERSION_CODES.TIRAMISU
30+
import android.os.Environment
3031
import androidx.lifecycle.Lifecycle
3132
import androidx.preference.PreferenceManager
3233
import androidx.test.core.app.ActivityScenario
3334
import androidx.test.core.app.ApplicationProvider
3435
import androidx.test.espresso.Espresso.onView
3536
import androidx.test.espresso.action.ViewActions
36-
import androidx.test.espresso.matcher.ViewMatchers.withId
3737
import androidx.test.espresso.matcher.ViewMatchers.withText
3838
import androidx.test.ext.junit.runners.AndroidJUnit4
3939
import androidx.test.rule.GrantPermissionRule
@@ -55,7 +55,7 @@ import java.io.File
5555

5656
@RunWith(AndroidJUnit4::class)
5757
class BackupPrefsFragmentTest {
58-
var storagePath = "/storage/emulated/0"
58+
var storagePath = Environment.getExternalStorageDirectory().absolutePath
5959
var fileName = "amaze_backup.json"
6060

6161
@Rule
@@ -93,6 +93,23 @@ class BackupPrefsFragmentTest {
9393
import(exportFile)
9494
}
9595

96+
/**
97+
* Waits (with a timeout) for the given file to exist, since some writes to storage happen
98+
* asynchronously on a background thread.
99+
*/
100+
private fun waitForFile(
101+
file: File,
102+
timeoutMillis: Long = 5000L,
103+
) {
104+
val startTime = System.currentTimeMillis()
105+
while (!file.exists()) {
106+
if (System.currentTimeMillis() - startTime > timeoutMillis) {
107+
fail("Timed out waiting for file to be created: ${file.absolutePath}")
108+
}
109+
Thread.sleep(100L)
110+
}
111+
}
112+
96113
/**
97114
* Test whether the exported file contains the expected preference values
98115
*/
@@ -103,58 +120,67 @@ class BackupPrefsFragmentTest {
103120
val backupPrefsFragment = BackupPrefsFragment()
104121
val activityScenario = ActivityScenario.launch(PreferencesActivity::class.java)
105122

106-
activityScenario.moveToState(Lifecycle.State.STARTED)
123+
// Espresso requires an activity to be RESUMED to dispatch view actions/clicks.
124+
activityScenario.moveToState(Lifecycle.State.RESUMED)
125+
126+
lateinit var preferences: SharedPreferences
107127

108-
activityScenario.onActivity {
109-
it.supportFragmentManager.beginTransaction()
128+
activityScenario.onActivity { preferencesActivity ->
129+
preferencesActivity.supportFragmentManager.beginTransaction()
110130
.add(backupPrefsFragment, null)
111131
.commitNow()
112132

113133
backupPrefsFragment.exportPrefs()
114-
}
115134

116-
val tempFile = File("${context.cacheDir.absolutePath}${File.separator}$fileName")
135+
val tempFile = File("${context.cacheDir.absolutePath}${File.separator}$fileName")
117136

118-
assertTrue(tempFile.exists())
119-
120-
onView(withId(R.id.home)).perform(ViewActions.click())
121-
onView(withText(R.string.save)).perform(ViewActions.click())
137+
assertTrue(tempFile.exists())
122138

123-
assertTrue(exportFile.exists())
124-
125-
activityScenario.onActivity { preferencesActivity ->
126-
val preferences = PreferenceManager.getDefaultSharedPreferences(preferencesActivity)
127-
val preferenceMap: Map<String?, *> = preferences.all
128-
129-
val inputString =
130-
exportFile
131-
.inputStream()
132-
.bufferedReader()
133-
.use {
134-
it.readText()
135-
}
136-
137-
val type = object : TypeToken<Map<String?, *>>() {}.type
139+
preferences = PreferenceManager.getDefaultSharedPreferences(preferencesActivity)
140+
}
138141

139-
val importMap: Map<String?, *> =
140-
GsonBuilder()
141-
.create()
142-
.fromJson(
143-
inputString,
144-
type,
145-
)
146-
147-
for ((key, value) in preferenceMap) {
148-
val importedValue = importMap[key]
149-
val mapValue =
150-
if (importedValue != null && importedValue::class.simpleName.equals("Double")) {
151-
(importedValue as Double).toInt() // since Gson parses Integer as Double
152-
} else {
153-
importedValue
154-
}
142+
// Espresso's onView().perform() must run on the instrumentation/test thread, never from
143+
// inside onActivity {} or runOnUiThread {} (both of which run on the main/UI thread).
144+
// Espresso internally synchronizes with the UI thread itself; calling it from the UI
145+
// thread can deadlock or throw IllegalStateException.
146+
// exportPrefs() launches MainActivity with an ACTION_SEND intent, which shows a Snackbar
147+
// with a "Save" action; that is the only view action needed here.
148+
onView(withText(R.string.save)).perform(ViewActions.click())
155149

156-
assertEquals("Difference found at key $key", value, mapValue)
157-
}
150+
// The actual write to storagePath happens asynchronously (RxJava) after the "Save" click
151+
// and after MainActivity finishes, so poll for the file instead of asserting immediately.
152+
waitForFile(exportFile)
153+
154+
val preferenceMap: Map<String?, *> = preferences.all
155+
156+
val inputString =
157+
exportFile
158+
.inputStream()
159+
.bufferedReader()
160+
.use {
161+
it.readText()
162+
}
163+
164+
val type = object : TypeToken<Map<String?, *>>() {}.type
165+
166+
val importMap: Map<String?, *> =
167+
GsonBuilder()
168+
.create()
169+
.fromJson(
170+
inputString,
171+
type,
172+
)
173+
174+
for ((key, value) in preferenceMap) {
175+
val importedValue = importMap[key]
176+
val mapValue =
177+
if (importedValue != null && importedValue::class.simpleName.equals("Double")) {
178+
(importedValue as Double).toInt() // since Gson parses Integer as Double
179+
} else {
180+
importedValue
181+
}
182+
183+
assertEquals("Difference found at key $key", value, mapValue)
158184
}
159185
}
160186

0 commit comments

Comments
 (0)