From b14d22550bd161594407ff06826ce8371a1dd075 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Sep 2020 15:12:25 +0200 Subject: [PATCH] PR Review Cleanup and Add command line to run the UI tests --- CHANGES.md | 3 +- docs/ui-tests.md | 40 ++++++++++++++----- .../android/sdk/common/CommonTestHelper.kt | 2 +- .../crypto/store/db/RealmCryptoStore.kt | 2 +- vector/build.gradle | 22 +++++----- .../app/{ExpressoExt.kt => EspressoExt.kt} | 36 +++++++++-------- .../java/im/vector/app/RegistrationTest.kt | 4 +- .../im/vector/app/VerificationTestBase.kt | 2 +- .../im/vector/app/core/utils/SystemUtils.kt | 4 ++ .../app/features/popup/PopupAlertManager.kt | 10 ++--- 10 files changed, 75 insertions(+), 50 deletions(-) rename vector/src/androidTest/java/im/vector/app/{ExpressoExt.kt => EspressoExt.kt} (82%) diff --git a/CHANGES.md b/CHANGES.md index e5542996ff..efbab5bd27 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,7 @@ Build 🧱: - Other changes: - - + - Added registration/verification automated UI tests Changes in Element 1.0.8 (2020-09-25) =================================================== @@ -49,7 +49,6 @@ SDK API changes ⚠️: Other changes: - Add an advanced action to reset an account data entry - - Added registration/verification automated UI tests Changes in Element 1.0.7 (2020-09-17) =================================================== diff --git a/docs/ui-tests.md b/docs/ui-tests.md index 9757481991..ff01da0b31 100644 --- a/docs/ui-tests.md +++ b/docs/ui-tests.md @@ -16,20 +16,20 @@ Out of the box, the tests use one of the homeservers (located at http://localhos You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. If you have already installed all dependencies, the steps are: -``` +```shell script $ git clone https://github.com/matrix-org/synapse.git $ cd synapse $ virtualenv -p python3 env $ source env/bin/activate -(env) $ python -m pip install --no-use-pep517 -e .` +(env) $ python -m pip install --no-use-pep517 -e . ``` Every time you want to launch these test homeservers, type: -``` +```shell script $ virtualenv -p python3 env $ source env/bin/activate -(env) $ demo/start.sh --no-rate-limit` +(env) $ demo/start.sh --no-rate-limit ``` **Emulator/Device set up** @@ -50,33 +50,53 @@ On your device, under **Settings > Developer options**, disable the following 3 - Transition animation scale - Animator duration scale +## Run the tests +Once Synapse is running, and an emulator is running, you can run the UI tests. + +### From the source code + +Click on the green arrow in front of each test. Clicking on the arrow in front of the test class, or from the package directory does not always work (Tests not found issue). + +### From command line + +````shell script +./gradlew vector:connectedGplayDebugAndroidTest +```` + +To run all the tests from the `vector` module. + +In case of trouble, you can try to uninstall the previous installed test APK first with this command: + +```shell script +adb uninstall im.vector.app.debug.test +``` ## Recipes We added some specific Espresso IdlingResources, and other utilities for matrix related tests ### Wait for initial sync -```` +```kotlin // Wait for initial sync and check room list is there withIdlingResource(initialSyncIdlingResource(uiSession)) { onView(withId(R.id.roomListContainer)) .check(matches(isDisplayed())) } -```` +``` ### Accessing current activity -```` +```kotlin val activity = EspressoHelper.getCurrentActivity()!! val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession() -```` +``` ### Interact with other session It's possible to create a session via the SDK, and then use this session to interact with the one that the emulator is using (to check verifications for example) -```` +```kotlin @Before fun initAccount() { val context = InstrumentationRegistry.getInstrumentation().targetContext @@ -84,4 +104,4 @@ fun initAccount() { val userName = "foobar_${System.currentTimeMillis()}" existingSession = createAccountAndSync(matrix, userName, password, true) } -````` +``` diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index fdbfa57b5c..84e76cbe52 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -218,7 +218,7 @@ class CommonTestHelper(context: Context) { .createAccount(userName, password, null, it) } - // Preform dummy step + // Perform dummy step val registrationResult = doSync { matrix.authenticationService .getRegistrationWizard() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 24de3cfe63..df71ef9eba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -372,7 +372,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { - Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null} ") + Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") doRealmTransaction(realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk diff --git a/vector/build.gradle b/vector/build.gradle index f9e485a0f9..9191ce640c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -173,15 +173,12 @@ android { } } - // The following argument makes the Android Test Orchestrator run its // "pm clear" command after each test invocation. This command ensures // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - } - testOptions { // Disables animations during instrumented tests you run from the command line… // This property does not affect tests that you run using Android Studio.” @@ -297,6 +294,11 @@ dependencies { def arch_version = '2.1.0' def lifecycle_version = '2.2.0' + // Tests + def kluent_version = '1.44' + def androidxTest_version = '1.3.0' + def espresso_version = '3.3.0' + implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") @@ -437,20 +439,20 @@ dependencies { // TESTS testImplementation 'junit:junit:4.12' - testImplementation 'org.amshove.kluent:kluent-android:1.44' + testImplementation "org.amshove.kluent:kluent-android:$kluent_version" // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' // Activate when you want to check for leaks, from time to time. //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' - androidTestImplementation 'androidx.test:core:1.3.0' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation "androidx.test:core:$androidxTest_version" + androidTestImplementation "androidx.test:runner:$androidxTest_version" + androidTestImplementation "androidx.test:rules:$androidxTest_version" androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' - androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' + androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" + androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" + androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version" androidTestImplementation "androidx.arch.core:core-testing:$arch_version" // Plant Timber tree for test androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' diff --git a/vector/src/androidTest/java/im/vector/app/ExpressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt similarity index 82% rename from vector/src/androidTest/java/im/vector/app/ExpressoExt.kt rename to vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 5bd42bda39..d247d88caa 100644 --- a/vector/src/androidTest/java/im/vector/app/ExpressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -44,12 +44,14 @@ import java.util.concurrent.TimeoutException object EspressoHelper { fun getCurrentActivity(): Activity? { var currentActivity: Activity? = null - getInstrumentation().runOnMainSync { run { currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) } } + getInstrumentation().runOnMainSync { + currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) + } return currentActivity } } -fun waitForView(viewMatcher: Matcher, timeout: Long = 10000, waitForDisplayed: Boolean = true): ViewAction { +fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction { return object : ViewAction { override fun getConstraints(): Matcher { return Matchers.any(View::class.java) @@ -62,25 +64,25 @@ fun waitForView(viewMatcher: Matcher, timeout: Long = 10000, waitForDispla } override fun perform(uiController: UiController, view: View) { - System.out.println("*** waitForView 1 $view") + println("*** waitForView 1 $view") uiController.loopMainThreadUntilIdle() val startTime = System.currentTimeMillis() val endTime = startTime + timeout val visibleMatcher = isDisplayed() do { - System.out.println("*** waitForView loop $view end:$endTime currrent:${System.currentTimeMillis()}") + println("*** waitForView loop $view end:$endTime current:${System.currentTimeMillis()}") val viewVisible = TreeIterables.breadthFirstViewTraversal(view) .any { viewMatcher.matches(it) && visibleMatcher.matches(it) } - System.out.println("*** waitForView loop viewVisible:$viewVisible") + println("*** waitForView loop viewVisible:$viewVisible") if (viewVisible == waitForDisplayed) return - System.out.println("*** waitForView loop loopMainThreadForAtLeast...") + println("*** waitForView loop loopMainThreadForAtLeast...") uiController.loopMainThreadForAtLeast(50) - System.out.println("*** waitForView loop ...loopMainThreadForAtLeast") + println("*** waitForView loop ...loopMainThreadForAtLeast") } while (System.currentTimeMillis() < endTime) - System.out.println("*** waitForView timeout $view") + println("*** waitForView timeout $view") // Timeout happens. throw PerformException.Builder() .withActionDescription(this.description) @@ -136,24 +138,24 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource { val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) val isIdle = hasResumed || currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false - System.out.println("*** [$name] isIdleNow activityIdlingResource $currentActivity isIdle:$isIdle") + println("*** [$name] isIdleNow activityIdlingResource $currentActivity isIdle:$isIdle") return isIdle } override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { - System.out.println("*** [$name] registerIdleTransitionCallback $callback") + println("*** [$name] registerIdleTransitionCallback $callback") this.callback = callback // if (hasResumed) callback?.onTransitionToIdle() } override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) { - System.out.println("*** [$name] onActivityLifecycleChanged $activity $stage") + println("*** [$name] onActivityLifecycleChanged $activity $stage") currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) val isIdle = currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false - System.out.println("*** [$name] onActivityLifecycleChanged $currentActivity isIdle:$isIdle") + println("*** [$name] onActivityLifecycleChanged $currentActivity isIdle:$isIdle") if (isIdle) { hasResumed = true - System.out.println("*** [$name] onActivityLifecycleChanged callback: $callback") + println("*** [$name] onActivityLifecycleChanged callback: $callback") callback?.onTransitionToIdle() ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this) } @@ -164,10 +166,10 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource { } fun withIdlingResource(idlingResource: IdlingResource, block: (() -> Unit)) { - System.out.println("*** withIdlingResource register") + println("*** withIdlingResource register") IdlingRegistry.getInstance().register(idlingResource) block.invoke() - System.out.println("*** withIdlingResource unregister") + println("*** withIdlingResource unregister") IdlingRegistry.getInstance().unregister(idlingResource) } @@ -179,7 +181,7 @@ fun allSecretsKnownIdling(session: Session): IdlingResource { override fun getName() = "AllSecretsKnownIdling_${session.myUserId}" override fun isIdleNow(): Boolean { - System.out.println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}") + println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}") return privateKeysInfo?.allKnown() == true } @@ -188,7 +190,7 @@ fun allSecretsKnownIdling(session: Session): IdlingResource { } override fun onChanged(t: Optional?) { - System.out.println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}") + println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}") privateKeysInfo = t?.getOrNull() if (t?.getOrNull()?.allKnown() == true) { session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this) diff --git a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt index a290aeec80..b88356db59 100644 --- a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt +++ b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt @@ -48,7 +48,7 @@ class RegistrationTest { val password: String = "password" val homeServerUrl: String = "http://10.0.2.2:8080" - // Check splashcreen is there + // Check splashscreen is there onView(withId(R.id.loginSplashSubmit)) .check(matches(isDisplayed())) .check(matches(withText(R.string.login_splash_submit))) @@ -57,7 +57,7 @@ class RegistrationTest { onView(withId(R.id.loginSplashSubmit)) .perform(click()) - // Check that home server options are showned + // Check that home server options are shown onView(withId(R.id.loginServerTitle)) .check(matches(isDisplayed())) .check(matches(withText(R.string.login_server_title))) diff --git a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt index 20d0053a9f..2a1b6d802f 100644 --- a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt +++ b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt @@ -158,7 +158,7 @@ abstract class VerificationTestBase { .createAccount(userName, password, null, it) } - // Preform dummy step + // Perform dummy step val registrationResult = doSync { matrix.authenticationService() .getRegistrationWizard() diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index 0e722da34a..c1253e76d3 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -55,6 +55,10 @@ fun isAirplaneModeOn(context: Context): Boolean { return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 } +fun isAnimationDisabled(context: Context): Boolean { + return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 814a7ca16e..59386ffff0 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -20,7 +20,6 @@ import android.app.Activity import android.os.Build import android.os.Handler import android.os.Looper -import android.provider.Settings import android.view.View import android.widget.ImageView import com.tapadoo.alerter.Alerter @@ -28,6 +27,7 @@ import com.tapadoo.alerter.OnHideAlertListener import dagger.Lazy import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.utils.isAnimationDisabled import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.pin.PinActivity import im.vector.app.features.themes.ThemeUtils @@ -173,9 +173,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy