Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,16 +42,21 @@ object StoragePermissionHelper {
*/
@JvmStatic
fun grantManageStoragePermission() {
// Only need to run on Androids >= R
if (Build.VERSION.SDK_INT < VERSION_CODES.R) {
return
}

// 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 amazeResources = context.packageManager.getResourcesForApplication(context.packageName)
val grantPermissionExplanation = amazeResources.getString(R.string.grant_all_files_permission)
val grantPermissionHeader = amazeResources.getString(R.string.grantper)

if (device.hasObject(By.text(grantPermissionExplanation))) {
if (device.hasObject(By.text(grantPermissionHeader))) {
// First press Amaze's grant button
onView(withText(R.string.grant)).perform(click())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,19 +43,20 @@ 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
import org.junit.Assert.fail
import org.junit.Before
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: String = Environment.getExternalStorageDirectory().absolutePath
var fileName = "amaze_backup.json"

@Rule
Expand Down Expand Up @@ -93,6 +94,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(
Comment thread
TranceLove marked this conversation as resolved.
file: File,
timeoutSeconds: Long = 5L,
) {
await().atMost(timeoutSeconds, TimeUnit.SECONDS).until {
file.exists() && file.length() > 0L
}
}

/**
* Test whether the exported file contains the expected preference values
*/
Expand All @@ -103,59 +117,71 @@ 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)

lateinit var preferenceSnapshot: Map<String?, *>

activityScenario.onActivity {
it.supportFragmentManager.beginTransaction()
activityScenario.onActivity { preferencesActivity ->
preferencesActivity.supportFragmentManager.beginTransaction()
.add(backupPrefsFragment, null)
.commitNow()

val preferences = PreferenceManager.getDefaultSharedPreferences(preferencesActivity)
preferenceSnapshot = HashMap(preferences.all)

backupPrefsFragment.exportPrefs()
}

val tempFile = File("${context.cacheDir.absolutePath}${File.separator}$fileName")
val tempFile = File("${context.cacheDir.absolutePath}${File.separator}$fileName")

assertTrue(tempFile.exists())
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())

activityScenario.onActivity { preferencesActivity ->
val preferences = PreferenceManager.getDefaultSharedPreferences(preferencesActivity)
val preferenceMap: Map<String?, *> = preferences.all

val inputString =
exportFile
.inputStream()
.bufferedReader()
.use {
it.readText()
}

val type = object : TypeToken<Map<String?, *>>() {}.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
}

assertEquals("Difference found at key $key", value, mapValue)
// 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)

val inputString =
exportFile
.inputStream()
.bufferedReader()
.use {
it.readText()
}

val type = object : TypeToken<Map<String?, *>>() {}.type

// TODO This breaks the exported file's types, all Numbers get converted to Double
val importMap: Map<String?, *> =
GsonBuilder()
.create()
.fromJson(
inputString,
type,
)

for ((key, value) in preferenceSnapshot) {
val importedValue = importMap[key]

if (value is Number) {
// HACK GsonBuilder().create().fromJson() breaks Number types
assertEquals("Difference found at key $key", value.toDouble(), importedValue as Double, 0.1)
} else {
assertEquals("Different type at key $key", value?.javaClass, importedValue?.javaClass)

assertEquals("Difference found at key $key", value, importedValue)
}
}

activityScenario.close()
}

/**
Expand All @@ -172,7 +198,15 @@ class BackupPrefsFragmentTest {
.add(backupPrefsFragment, null)
.commitNow()

javaClass.getResourceAsStream("/$fileName")?.copyTo(exportFile.outputStream())
val resourceStream =
requireNotNull(javaClass.getResourceAsStream("/$fileName")) {
"Missing test resource /$fileName"
}
resourceStream.use { input ->
exportFile.outputStream().use { output ->
input.copyTo(output)
}
}

backupPrefsFragment.onActivityResult(
BackupPrefsFragment.IMPORT_BACKUP_FILE,
Expand Down Expand Up @@ -204,13 +238,14 @@ class BackupPrefsFragmentTest {
assertFalse(preferenceMap.containsKey(null))

for ((k, v) in preferenceMap) {
// This cast tells the kotlin type checker that fail() never returns
val key = k ?: (fail() as Nothing)
val value = v ?: (fail() as Nothing)
val key = requireNotNull(k) { "Preference key unexpectedly null" }
val value = requireNotNull(v) { "Preference value unexpectedly null for $key" }

assertTrue("checkPrefEqual($key) failed", checkPrefEqual(preferences, importMap, key, value))
}
}

activityScenario.close()
}

private fun checkPrefEqual(
Expand All @@ -223,15 +258,14 @@ class BackupPrefsFragmentTest {
"Boolean" -> return importMap[key] as Boolean ==
preferences.getBoolean(key, false)
"Float" ->
importMap[key] as Float ==
(importMap[key] as Number).toFloat() ==
preferences.getFloat(key, 0f)
"Int" -> {
// since Gson parses Integer as Double
val toInt = (importMap[key] as Double).toInt()
val toInt = (importMap[key] as Number).toInt()

return toInt == preferences.getInt(key, 0)
}
"Long" -> return importMap[key] as Long ==
"Long" -> return (importMap[key] as Number).toLong() ==
preferences.getLong(key, 0L)
"String" -> return importMap[key] as String ==
preferences.getString(key, null)
Expand Down
Loading
Loading