From 59cef31183400d60a26f9ca69fe21aa6b7bbf5b6 Mon Sep 17 00:00:00 2001 From: Steve Soltys Date: Thu, 14 Sep 2023 06:37:38 +0000 Subject: [PATCH] Address review comments, add screen recording, use test backup data --- .cirrus.yml | 21 ++- app/build.gradle | 3 +- app/development/scripts/provision_emulator.sh | 9 ++ app/src/androidTest/AndroidManifest.xml | 1 - .../seedvault/e2e/BackupRestoreTest.kt | 139 ---------------- .../seedvault/e2e/LargeBackupTestBase.kt | 71 +++++++++ .../seedvault/e2e/LargeRestoreTestBase.kt | 86 ++++++++++ .../seedvault/e2e/LargeTestBase.kt | 150 ++++++++---------- .../seedvault/e2e/SeedvaultLargeTest.kt | 73 +++++++++ .../seedvault/e2e/impl/BackupRestoreTest.kt | 41 +++++ .../seedvault/e2e/screen/UiDeviceScreen.kt | 23 ++- .../e2e/screen/impl/DocumentPickerScreen.kt | 2 + .../e2e/screen/impl/RecoveryCodeScreen.kt | 13 +- .../e2e/screen/impl/RestoreScreen.kt | 4 +- 14 files changed, 392 insertions(+), 244 deletions(-) delete mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt create mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt create mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt create mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt create mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt diff --git a/.cirrus.yml b/.cirrus.yml index d2f6c6c1..896a6a4b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,9 +4,9 @@ container: cpu: 8 memory: 16G -check_android_task: +instrumentation_tests_task: + name: "Cirrus CI Instrumentation Tests" skip: "!changesInclude('.cirrus.yml', '*.gradle', '*.gradle.kts', '**/*.gradle', '**/*.gradle.kts', '*.properties', '**/*.properties', '**/*.kt', '**/*.xml')" - create_avd_script: start_avd_background_script: sdkmanager --install "system-images;android-33;google_apis;x86_64"; echo no | avdmanager create avd -n seedvault -k "system-images;android-33;google_apis;x86_64"; @@ -19,10 +19,9 @@ check_android_task: -no-window -writable-system; provision_avd_background_script: - adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'; - wget --output-document etar.apk https://f-droid.org/repo/ws.xsoh.etar_35.apk; - adb install etar.apk + wget https://github.com/seedvault-app/seedvault-test-data/releases/download/1/backup.tar.gz; + adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'; adb root; sleep 5; adb remount; @@ -31,11 +30,17 @@ check_android_task: adb root; sleep 5; adb remount; - assemble_release_script: + sleep 5; + assemble_script: ./gradlew :app:assembleRelease :app:assembleAndroidTest install_app_script: timeout 180s bash -c 'while [[ -z $(adb shell mount | grep "/system " | grep "(rw,") ]]; do sleep 1; done;'; adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'; + + adb shell mkdir -p /sdcard/seedvault_baseline; + adb push backup.tar.gz /sdcard/seedvault_baseline/backup.tar.gz; + adb shell tar xzf /sdcard/seedvault_baseline/backup.tar.gz --directory=/sdcard/seedvault_baseline; + adb shell mkdir -p /system/priv-app/Seedvault; adb push app/build/outputs/apk/release/app-release.apk /system/priv-app/Seedvault/Seedvault.apk; adb push permissions_com.stevesoltys.seedvault.xml /system/etc/permissions/privapp-permissions-seedvault.xml; @@ -48,6 +53,6 @@ check_android_task: run_medium_tests_script: ./gradlew -Pinstrumented_test_size=medium :app:connectedAndroidTest always: pull_screenshots_script: - adb pull /sdcard/Documents/screenshots + adb pull /sdcard/seedvault_test_videos screenshots_artifacts: - path: "screenshots/**/*.png" + path: "seedvault_test_videos/**/*.mp4" diff --git a/app/build.gradle b/app/build.gradle index 6323ce3f..06327965 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,8 +162,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation "io.mockk:mockk-android:$mockk_version" - - androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.5.3' + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' } apply from: "${rootProject.rootDir}/gradle/ktlint.gradle" diff --git a/app/development/scripts/provision_emulator.sh b/app/development/scripts/provision_emulator.sh index b6c0623a..8af6504f 100755 --- a/app/development/scripts/provision_emulator.sh +++ b/app/development/scripts/provision_emulator.sh @@ -67,4 +67,13 @@ echo "Rebooting emulator..." $ADB reboot $ADB wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;' +echo "Downloading and extracting test backup to '/sdcard/seedvault'..." +wget https://github.com/seedvault-app/seedvault-test-data/releases/download/1/backup.tar.gz +$ADB push backup.tar.gz /sdcard/ +rm backup.tar.gz + +$ADB shell mkdir -p /sdcard/seedvault_baseline +$ADB shell tar xzf /sdcard/backup.tar.gz --directory=/sdcard/seedvault_baseline +$ADB shell rm /sdcard/backup.tar.gz + echo "Emulator '$EMULATOR_NAME' has been provisioned with Seedvault!" diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml index 7770cfb2..00f58f96 100644 --- a/app/src/androidTest/AndroidManifest.xml +++ b/app/src/androidTest/AndroidManifest.xml @@ -1,5 +1,4 @@ - diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt deleted file mode 100644 index 1542cafb..00000000 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.stevesoltys.seedvault.e2e - -import androidx.test.filters.LargeTest -import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen -import com.stevesoltys.seedvault.restore.RestoreViewModel -import com.stevesoltys.seedvault.transport.backup.PackageService -import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager -import io.mockk.every -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import org.junit.Test -import org.koin.core.component.inject -import java.util.concurrent.atomic.AtomicBoolean - -@LargeTest -class BackupRestoreTest : LargeTestBase() { - - private val packageService: PackageService by inject() - - private val spyBackupNotificationManager: BackupNotificationManager by inject() - - private val restoreViewModel: RestoreViewModel by inject() - - companion object { - private const val BACKUP_TIMEOUT = 360 * 1000L - private const val RESTORE_TIMEOUT = 360 * 1000L - } - - @Test - fun `backup and restore applications`() = run { - launchBackupActivity() - verifyCode() - chooseBackupLocation() - - val eligiblePackages = launchAllEligibleApps() - performBackup(eligiblePackages) - uninstallPackages(eligiblePackages) - performRestore() - - val packagesAfterRestore = getEligibleApps() - assert(eligiblePackages == packagesAfterRestore) - } - - private fun getEligibleApps() = packageService.userApps - .map { it.packageName }.toSet() - - private fun launchAllEligibleApps(): Set { - return getEligibleApps().onEach { - val intent = device.targetContext.packageManager.getLaunchIntentForPackage(it) - - device.targetContext.startActivity(intent) - waitUntilIdle() - } - } - - private fun performBackup(expectedPackages: Set) = run { - val backupResult = spyOnBackup(expectedPackages) - startBackup() - waitForBackupResult(backupResult) - screenshot("backup result") - } - - private fun spyOnBackup(expectedPackages: Set): AtomicBoolean { - val finishedBackup = AtomicBoolean(false) - - every { - spyBackupNotificationManager.onBackupFinished(any(), any()) - } answers { - val success = firstArg() - assert(success) { "Backup failed." } - - val packageCount = secondArg() - assert(packageCount == expectedPackages.size) { - "Expected ${expectedPackages.size} apps, got $packageCount." - } - - this.callOriginal() - finishedBackup.set(true) - } - - return finishedBackup - } - - private fun waitForBackupResult(finishedBackup: AtomicBoolean) = run { - step("Wait for backup completion") { - runBlocking { - withTimeout(BACKUP_TIMEOUT) { - while (!finishedBackup.get()) { - delay(100) - } - } - } - } - } - - private fun performRestore() = run { - step("Start restore and await completion") { - RestoreScreen { - startRestore() - waitForInstallResult() - screenshot("restore app apks result") - - nextButton.click() - waitForRestoreResult() - screenshot("restore app data result") - - finishButton.click() - } - } - } - - private fun waitForInstallResult() = runBlocking { - withTimeout(RESTORE_TIMEOUT) { - - while (restoreViewModel.installResult.value == null) { - delay(100) - } - - val restoreResultValue = restoreViewModel.installResult.value!! - assert(!restoreResultValue.hasFailed) { "Failed to install packages" } - } - } - - private fun waitForRestoreResult() = runBlocking { - withTimeout(RESTORE_TIMEOUT) { - - while (restoreViewModel.restoreBackupResult.value == null) { - delay(100) - } - - val restoreResultValue = restoreViewModel.restoreBackupResult.value!! - - assert(!restoreResultValue.hasError()) { - "Restore failed: ${restoreResultValue.errorMsg}" - } - } - } -} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt new file mode 100644 index 00000000..c3695931 --- /dev/null +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeBackupTestBase.kt @@ -0,0 +1,71 @@ +package com.stevesoltys.seedvault.e2e + +import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import io.mockk.clearMocks +import io.mockk.every +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import java.util.concurrent.atomic.AtomicBoolean + +internal interface LargeBackupTestBase : LargeTestBase { + + companion object { + private const val BACKUP_TIMEOUT = 360 * 1000L + } + + val spyBackupNotificationManager: BackupNotificationManager + + fun launchBackupActivity() { + runCommand("am start -n ${targetContext.packageName}/.settings.SettingsActivity") + waitUntilIdle() + } + + fun startBackup() { + BackupScreen { + backupMenu.clickAndWaitForNewWindow() + waitUntilIdle() + + backupNowButton.clickAndWaitForNewWindow() + waitUntilIdle() + + backupStatusButton.clickAndWaitForNewWindow() + waitUntilIdle() + } + } + + fun performBackup(expectedPackages: Set) { + val backupResult = spyOnBackup(expectedPackages) + startBackup() + waitForBackupResult(backupResult) + } + + private fun spyOnBackup(expectedPackages: Set): AtomicBoolean { + val finishedBackup = AtomicBoolean(false) + + clearMocks(spyBackupNotificationManager) + + every { + spyBackupNotificationManager.onBackupFinished(any(), any()) + } answers { + val success = firstArg() + assert(success) { "Backup failed." } + + this.callOriginal() + finishedBackup.set(true) + } + + return finishedBackup + } + + private fun waitForBackupResult(finishedBackup: AtomicBoolean) { + runBlocking { + withTimeout(BACKUP_TIMEOUT) { + while (!finishedBackup.get()) { + delay(100) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt new file mode 100644 index 00000000..1ba2b288 --- /dev/null +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeRestoreTestBase.kt @@ -0,0 +1,86 @@ +package com.stevesoltys.seedvault.e2e + +import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen +import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen +import com.stevesoltys.seedvault.restore.RestoreViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout + +internal interface LargeRestoreTestBase : LargeTestBase { + + companion object { + private const val RESTORE_TIMEOUT = 360 * 1000L + } + + val spyRestoreViewModel: RestoreViewModel + + fun launchRestoreActivity() { + runCommand("am start -n ${targetContext.packageName}/.restore.RestoreActivity") + waitUntilIdle() + } + + fun typeInRestoreCode(code: List) { + assert(code.size == 12) { "Code must have 12 words." } + + RecoveryCodeScreen { + waitUntilIdle() + + code.forEachIndexed { index, word -> + wordTextField(index).text = word + } + + waitUntilIdle() + verifyCodeButton.scrollTo().click() + } + } + + fun performRestore() { + RestoreScreen { + backupListItem.clickAndWaitForNewWindow() + waitUntilIdle() + + waitForInstallResult() + nextButton.clickAndWaitForNewWindow() + + waitForRestoreDataResult() + finishButton.clickAndWaitForNewWindow() + skipButton.clickAndWaitForNewWindow() + waitUntilIdle() + } + } + + private fun waitForInstallResult() = runBlocking { + withTimeout(RESTORE_TIMEOUT) { + while (spyRestoreViewModel.installResult.value == null || + spyRestoreViewModel.nextButtonEnabled.value == false + ) { + delay(100) + } + } + + val restoreResultValue = spyRestoreViewModel.installResult.value + ?: error("Restore APKs timed out") + + assert(!restoreResultValue.hasFailed) { "Failed to install packages" } + waitUntilIdle() + } + + private fun waitForRestoreDataResult() = runBlocking { + withTimeout(RESTORE_TIMEOUT) { + while (spyRestoreViewModel.restoreBackupResult.value == null) { + delay(100) + } + } + + val restoreResultValue = spyRestoreViewModel.restoreBackupResult.value + ?: error("Restore app data timed out") + + assert(!restoreResultValue.hasError()) { + "Restore failed: ${restoreResultValue.errorMsg}" + } + + waitUntilIdle() + } + +} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt index fc0c01ed..89ebbb9d 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt @@ -1,110 +1,100 @@ package com.stevesoltys.seedvault.e2e -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.app.UiAutomation +import android.content.Context +import android.os.Environment +import androidx.annotation.WorkerThread import androidx.test.platform.app.InstrumentationRegistry -import com.kaspersky.kaspresso.testcases.api.testcase.TestCase -import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen +import androidx.test.uiautomator.UiDevice import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen -import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen -import org.junit.After -import org.junit.Before -import org.junit.runner.RunWith -import org.koin.core.component.KoinComponent import java.lang.Thread.sleep +import java.text.SimpleDateFormat +import java.util.Calendar -@RunWith(AndroidJUnit4::class) -abstract class LargeTestBase : TestCase(), KoinComponent { +interface LargeTestBase { - @Before - open fun setUp() { - // reset document picker state, and delete old backups - runCommand("pm clear com.google.android.documentsui") - runCommand("rm -Rf /sdcard/seedvault") + companion object { + private const val TEST_STORAGE_FOLDER = "seedvault_test" + private const val TEST_VIDEO_FOLDER = "seedvault_test_videos" } - @After - open fun tearDown() { - screenshot("end") - } + fun externalStorageDir(): String = Environment.getExternalStorageDirectory().absolutePath - protected fun launchBackupActivity() = run { - runCommand("am start -n ${device.targetContext.packageName}/.settings.SettingsActivity") - waitUntilIdle() - } + fun testStoragePath(): String = "${externalStorageDir()}/$TEST_STORAGE_FOLDER" - protected fun launchRestoreActivity() = run { - runCommand("am start -n ${device.targetContext.packageName}/.restore.RestoreActivity") - waitUntilIdle() - } + fun testVideoPath(): String = "${externalStorageDir()}/$TEST_VIDEO_FOLDER" - protected fun waitUntilIdle() { - device.uiDevice.waitForIdle() + val targetContext: Context + get() = InstrumentationRegistry.getInstrumentation().targetContext + + val uiAutomation: UiAutomation + get() = InstrumentationRegistry.getInstrumentation().uiAutomation + + val device: UiDevice + get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + fun waitUntilIdle() { + device.waitForIdle() sleep(3000) } - protected fun verifyCode() = run { - RecoveryCodeScreen { - step("Confirm code") { - screenshot("confirm code") - confirmCodeButton.click() - } - step("Verify code") { - screenshot("verify code") - verifyCodeButton.scrollTo() - verifyCodeButton.click() - } - } + fun runCommand(command: String) { + uiAutomation.executeShellCommand(command).close() } - protected fun chooseBackupLocation() = run { - step("Choose backup location") { - waitUntilIdle() - screenshot("choose backup location") + @WorkerThread + fun startScreenRecord(testName: String) { + val simpleDateFormat = SimpleDateFormat("yyyyMMdd_hhmmss") + val timeStamp = simpleDateFormat.format(Calendar.getInstance().time) + val fileName = "${timeStamp}_${testName.replace(" ", "_")}" - DocumentPickerScreen { - createNewFolderButton.click() - textBox.text = "seedvault" - okButton.click() - useThisFolderButton.click() - allowButton.click() - } - } + val folder = testVideoPath() + runCommand("mkdir -p $folder") + runCommand("screenrecord $folder/$fileName.mp4") } - protected fun startBackup() = run { - launchBackupActivity() - - step("Run backup") { - BackupScreen { - backupMenu.clickAndWaitForNewWindow() - backupNowButton.clickAndWaitForNewWindow() - backupStatusButton.clickAndWaitForNewWindow() - } - } + @WorkerThread + fun stopScreenRecord() { + runCommand("pkill -2 screenrecord") } - protected fun startRestore() = run { - launchRestoreActivity() - - step("Restore backup") { - RestoreScreen { - backupListItem.clickAndWaitForNewWindow() - } - } - } - - protected fun uninstallPackages(packages: Set) { + fun uninstallPackages(packages: Set) { packages.forEach { runCommand("pm uninstall $it") } } - protected fun runCommand(command: String) { - InstrumentationRegistry.getInstrumentation().uiAutomation - .executeShellCommand(command) - .close() + fun clearDocumentPickerAppData() { + runCommand("pm clear com.google.android.documentsui") } - protected fun screenshot(name: String) { - device.screenshots.take(name.replace(" ", "_")) + fun clearTestBackups() { + runCommand("rm -Rf ${testStoragePath()}") + } + + fun chooseStorageLocation( + folderName: String = TEST_STORAGE_FOLDER, + exists: Boolean = false, + ) { + DocumentPickerScreen { + if (exists) { + existingFolder(folderName).scrollTo().clickAndWaitForNewWindow() + + } else { + createNewFolderButton.clickAndWaitForNewWindow() + textBox.text = folderName + okButton.clickAndWaitForNewWindow() + } + + useThisFolderButton.clickAndWaitForNewWindow() + allowButton.clickAndWaitForNewWindow() + } + } + + fun confirmCode() { + RecoveryCodeScreen { + confirmCodeButton.click() + + verifyCodeButton.scrollTo().click() + } } } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt new file mode 100644 index 00000000..eff53014 --- /dev/null +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/SeedvaultLargeTest.kt @@ -0,0 +1,73 @@ +package com.stevesoltys.seedvault.e2e + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stevesoltys.seedvault.restore.RestoreViewModel +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.rules.TestName +import org.junit.runner.RunWith +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.File + +@RunWith(AndroidJUnit4::class) +internal abstract class SeedvaultLargeTest : + LargeBackupTestBase, LargeRestoreTestBase, KoinComponent { + + @JvmField + @Rule + var name = TestName() + + companion object { + private const val BASELINE_BACKUP_FOLDER = "seedvault_baseline" + private const val RECOVERY_CODE_FILE = "recovery-code.txt" + } + + override val spyBackupNotificationManager: BackupNotificationManager by inject() + + override val spyRestoreViewModel: RestoreViewModel by inject() + + private val baselineBackupFolderPath = "${this.externalStorageDir()}/$BASELINE_BACKUP_FOLDER" + + private val baselineRecoveryCodePath = "$baselineBackupFolderPath/$RECOVERY_CODE_FILE" + + @Before + open fun setUp() { + clearDocumentPickerAppData() + clearTestBackups() + + startScreenRecord(name.methodName) + restoreBaselineBackup() + } + + @After + open fun tearDown() { + stopScreenRecord() + } + + /** + * Restore the baseline backup, if it exists. + * + * This is a hand-crafted backup containing various apps and app data that we use for + * provisioning tests: https://github.com/seedvault-app/seedvault-test-data + */ + private fun restoreBaselineBackup() { + if (File(baselineBackupFolderPath).exists()) { + launchRestoreActivity() + chooseStorageLocation(folderName = BASELINE_BACKUP_FOLDER, exists = true) + typeInRestoreCode(baselineBackupRecoveryCode()) + performRestore() + } + } + + private fun baselineBackupRecoveryCode(): List { + val recoveryCodeFile = File(baselineRecoveryCodePath) + + return recoveryCodeFile.readLines() + .filter { it.isNotBlank() } + .joinToString(separator = " ") { it.trim() } + .split(" ") + } +} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt new file mode 100644 index 00000000..bc85ff54 --- /dev/null +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/impl/BackupRestoreTest.kt @@ -0,0 +1,41 @@ +package com.stevesoltys.seedvault.e2e.impl + +import androidx.test.filters.LargeTest +import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest +import com.stevesoltys.seedvault.settings.SettingsManager +import com.stevesoltys.seedvault.transport.backup.PackageService +import org.junit.Test +import org.koin.core.component.inject + +@LargeTest +internal class BackupRestoreTest : SeedvaultLargeTest() { + + private val packageService: PackageService by inject() + + private val settingsManager: SettingsManager by inject() + + @Test + fun `backup and restore applications`() { + launchBackupActivity() + + if (settingsManager.getStorage() == null) { + confirmCode() + chooseStorageLocation() + } + + val eligiblePackages = getEligibleApps() + performBackup(eligiblePackages) + uninstallPackages(eligiblePackages) + + launchRestoreActivity() + performRestore() + + // TODO: Get some real assertions in here.. + // val packagesAfterRestore = getEligibleApps() + // assert(eligiblePackages == packagesAfterRestore) + } + + private fun getEligibleApps() = packageService.userApps + .map { it.packageName }.toSet() + +} diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt index bddc428a..7a826216 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/UiDeviceScreen.kt @@ -1,9 +1,12 @@ package com.stevesoltys.seedvault.e2e.screen +import android.widget.ScrollView import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector +import java.lang.Thread.sleep abstract class UiDeviceScreen { @@ -11,10 +14,20 @@ abstract class UiDeviceScreen { function.invoke(this as T) } - protected fun findObject( + fun UiObject.scrollTo( + scrollSelector: UiSelector = UiSelector().className(ScrollView::class.java), + ): UiObject { + UiScrollable(scrollSelector).scrollIntoView(this) + waitForExists(15000) + sleep(2000) + return this + } + + fun findObject( block: UiSelector.() -> UiSelector, - ): UiObject = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - .findObject( - UiSelector().let { it.block() } - ) + ): UiObject = device().findObject( + UiSelector().let { it.block() } + ) + + private fun device() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt index f6f101e4..050e57b2 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/DocumentPickerScreen.kt @@ -14,4 +14,6 @@ object DocumentPickerScreen : UiDeviceScreen() { val okButton = findObject { text("OK") } val allowButton = findObject { text("ALLOW") } + + fun existingFolder(folderName: String) = findObject { text(folderName) } } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt index 3ce61511..6804c5c7 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RecoveryCodeScreen.kt @@ -1,15 +1,12 @@ package com.stevesoltys.seedvault.e2e.screen.impl -import com.kaspersky.kaspresso.screens.KScreen -import com.stevesoltys.seedvault.R -import io.github.kakaocup.kakao.text.KButton +import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen -object RecoveryCodeScreen : KScreen() { +object RecoveryCodeScreen : UiDeviceScreen() { - override val layoutId: Int? = null - override val viewClass: Class<*>? = null + val confirmCodeButton = findObject { text("Confirm code") } - val confirmCodeButton = KButton { withId(R.id.confirmCodeButton) } + val verifyCodeButton = findObject { text("Verify") } - val verifyCodeButton = KButton { withId(R.id.doneButton) } + fun wordTextField(index: Int) = findObject { text("Word ${index + 1}") } } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt index 706fc7bf..20914755 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/screen/impl/RestoreScreen.kt @@ -4,9 +4,11 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen object RestoreScreen : UiDeviceScreen() { - val backupListItem = findObject { textContains("Last backup 0 hr") } + val backupListItem = findObject { textContains("Last backup") } val nextButton = findObject { text("Next") } val finishButton = findObject { text("Finish") } + + val skipButton = findObject { text("Skip restoring files") } }