PR Review

Cleanup and Add command line to run the UI tests
This commit is contained in:
Benoit Marty 2020-09-29 15:12:25 +02:00
parent f79784bc8c
commit b14d22550b
10 changed files with 75 additions and 50 deletions

View file

@ -20,7 +20,7 @@ Build 🧱:
- -
Other changes: Other changes:
- - Added registration/verification automated UI tests
Changes in Element 1.0.8 (2020-09-25) Changes in Element 1.0.8 (2020-09-25)
=================================================== ===================================================
@ -49,7 +49,6 @@ SDK API changes ⚠️:
Other changes: Other changes:
- Add an advanced action to reset an account data entry - 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) Changes in Element 1.0.7 (2020-09-17)
=================================================== ===================================================

View file

@ -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: 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 $ git clone https://github.com/matrix-org/synapse.git
$ cd synapse $ cd synapse
$ virtualenv -p python3 env $ virtualenv -p python3 env
$ source env/bin/activate $ 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: Every time you want to launch these test homeservers, type:
``` ```shell script
$ virtualenv -p python3 env $ virtualenv -p python3 env
$ source env/bin/activate $ source env/bin/activate
(env) $ demo/start.sh --no-rate-limit` (env) $ demo/start.sh --no-rate-limit
``` ```
**Emulator/Device set up** **Emulator/Device set up**
@ -50,33 +50,53 @@ On your device, under **Settings > Developer options**, disable the following 3
- Transition animation scale - Transition animation scale
- Animator duration 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 ## Recipes
We added some specific Espresso IdlingResources, and other utilities for matrix related tests We added some specific Espresso IdlingResources, and other utilities for matrix related tests
### Wait for initial sync ### Wait for initial sync
```` ```kotlin
// Wait for initial sync and check room list is there // Wait for initial sync and check room list is there
withIdlingResource(initialSyncIdlingResource(uiSession)) { withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer)) onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
} }
```` ```
### Accessing current activity ### Accessing current activity
```` ```kotlin
val activity = EspressoHelper.getCurrentActivity()!! val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession() val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
```` ```
### Interact with other session ### 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) 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 @Before
fun initAccount() { fun initAccount() {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
@ -84,4 +104,4 @@ fun initAccount() {
val userName = "foobar_${System.currentTimeMillis()}" val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
} }
````` ```

View file

@ -218,7 +218,7 @@ class CommonTestHelper(context: Context) {
.createAccount(userName, password, null, it) .createAccount(userName, password, null, it)
} }
// Preform dummy step // Perform dummy step
val registrationResult = doSync<RegistrationResult> { val registrationResult = doSync<RegistrationResult> {
matrix.authenticationService matrix.authenticationService
.getRegistrationWizard() .getRegistrationWizard()

View file

@ -372,7 +372,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { 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 -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk xSignMasterPrivateKey = msk

View file

@ -173,15 +173,12 @@ android {
} }
} }
// The following argument makes the Android Test Orchestrator run its // The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures // "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
} }
testOptions { testOptions {
// Disables animations during instrumented tests you run from the command line // Disables animations during instrumented tests you run from the command line
// This property does not affect tests that you run using Android Studio. // This property does not affect tests that you run using Android Studio.
@ -297,6 +294,11 @@ dependencies {
def arch_version = '2.1.0' def arch_version = '2.1.0'
def lifecycle_version = '2.2.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")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch") implementation project(":diff-match-patch")
@ -437,20 +439,20 @@ dependencies {
// TESTS // TESTS
testImplementation 'junit:junit:4.12' 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 // Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
// Activate when you want to check for leaks, from time to time. // Activate when you want to check for leaks, from time to time.
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation "androidx.test:core:$androidxTest_version"
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation "androidx.test:runner:$androidxTest_version"
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation "androidx.test:rules:$androidxTest_version"
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version"
androidTestImplementation "androidx.arch.core:core-testing:$arch_version" androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
// Plant Timber tree for test // Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'

View file

@ -44,12 +44,14 @@ import java.util.concurrent.TimeoutException
object EspressoHelper { object EspressoHelper {
fun getCurrentActivity(): Activity? { fun getCurrentActivity(): Activity? {
var currentActivity: Activity? = null 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 return currentActivity
} }
} }
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10000, waitForDisplayed: Boolean = true): ViewAction { fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
return object : ViewAction { return object : ViewAction {
override fun getConstraints(): Matcher<View> { override fun getConstraints(): Matcher<View> {
return Matchers.any(View::class.java) return Matchers.any(View::class.java)
@ -62,25 +64,25 @@ fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10000, waitForDispla
} }
override fun perform(uiController: UiController, view: View) { override fun perform(uiController: UiController, view: View) {
System.out.println("*** waitForView 1 $view") println("*** waitForView 1 $view")
uiController.loopMainThreadUntilIdle() uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val endTime = startTime + timeout val endTime = startTime + timeout
val visibleMatcher = isDisplayed() val visibleMatcher = isDisplayed()
do { 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) val viewVisible = TreeIterables.breadthFirstViewTraversal(view)
.any { viewMatcher.matches(it) && visibleMatcher.matches(it) } .any { viewMatcher.matches(it) && visibleMatcher.matches(it) }
System.out.println("*** waitForView loop viewVisible:$viewVisible") println("*** waitForView loop viewVisible:$viewVisible")
if (viewVisible == waitForDisplayed) return if (viewVisible == waitForDisplayed) return
System.out.println("*** waitForView loop loopMainThreadForAtLeast...") println("*** waitForView loop loopMainThreadForAtLeast...")
uiController.loopMainThreadForAtLeast(50) uiController.loopMainThreadForAtLeast(50)
System.out.println("*** waitForView loop ...loopMainThreadForAtLeast") println("*** waitForView loop ...loopMainThreadForAtLeast")
} while (System.currentTimeMillis() < endTime) } while (System.currentTimeMillis() < endTime)
System.out.println("*** waitForView timeout $view") println("*** waitForView timeout $view")
// Timeout happens. // Timeout happens.
throw PerformException.Builder() throw PerformException.Builder()
.withActionDescription(this.description) .withActionDescription(this.description)
@ -136,24 +138,24 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource {
val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
val isIdle = hasResumed || currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false 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 return isIdle
} }
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
System.out.println("*** [$name] registerIdleTransitionCallback $callback") println("*** [$name] registerIdleTransitionCallback $callback")
this.callback = callback this.callback = callback
// if (hasResumed) callback?.onTransitionToIdle() // if (hasResumed) callback?.onTransitionToIdle()
} }
override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) { 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) currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0)
val isIdle = currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false 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) { if (isIdle) {
hasResumed = true hasResumed = true
System.out.println("*** [$name] onActivityLifecycleChanged callback: $callback") println("*** [$name] onActivityLifecycleChanged callback: $callback")
callback?.onTransitionToIdle() callback?.onTransitionToIdle()
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this) ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this)
} }
@ -164,10 +166,10 @@ fun activityIdlingResource(activityClass: Class<*>): IdlingResource {
} }
fun withIdlingResource(idlingResource: IdlingResource, block: (() -> Unit)) { fun withIdlingResource(idlingResource: IdlingResource, block: (() -> Unit)) {
System.out.println("*** withIdlingResource register") println("*** withIdlingResource register")
IdlingRegistry.getInstance().register(idlingResource) IdlingRegistry.getInstance().register(idlingResource)
block.invoke() block.invoke()
System.out.println("*** withIdlingResource unregister") println("*** withIdlingResource unregister")
IdlingRegistry.getInstance().unregister(idlingResource) IdlingRegistry.getInstance().unregister(idlingResource)
} }
@ -179,7 +181,7 @@ fun allSecretsKnownIdling(session: Session): IdlingResource {
override fun getName() = "AllSecretsKnownIdling_${session.myUserId}" override fun getName() = "AllSecretsKnownIdling_${session.myUserId}"
override fun isIdleNow(): Boolean { override fun isIdleNow(): Boolean {
System.out.println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}") println("*** [$name]/isIdleNow allSecretsKnownIdling ${privateKeysInfo?.allKnown()}")
return privateKeysInfo?.allKnown() == true return privateKeysInfo?.allKnown() == true
} }
@ -188,7 +190,7 @@ fun allSecretsKnownIdling(session: Session): IdlingResource {
} }
override fun onChanged(t: Optional<PrivateKeysInfo>?) { override fun onChanged(t: Optional<PrivateKeysInfo>?) {
System.out.println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}") println("*** [$name] allSecretsKnownIdling ${t?.getOrNull()}")
privateKeysInfo = t?.getOrNull() privateKeysInfo = t?.getOrNull()
if (t?.getOrNull()?.allKnown() == true) { if (t?.getOrNull()?.allKnown() == true) {
session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this) session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().removeObserver(this)

View file

@ -48,7 +48,7 @@ class RegistrationTest {
val password: String = "password" val password: String = "password"
val homeServerUrl: String = "http://10.0.2.2:8080" val homeServerUrl: String = "http://10.0.2.2:8080"
// Check splashcreen is there // Check splashscreen is there
onView(withId(R.id.loginSplashSubmit)) onView(withId(R.id.loginSplashSubmit))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
.check(matches(withText(R.string.login_splash_submit))) .check(matches(withText(R.string.login_splash_submit)))
@ -57,7 +57,7 @@ class RegistrationTest {
onView(withId(R.id.loginSplashSubmit)) onView(withId(R.id.loginSplashSubmit))
.perform(click()) .perform(click())
// Check that home server options are showned // Check that home server options are shown
onView(withId(R.id.loginServerTitle)) onView(withId(R.id.loginServerTitle))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
.check(matches(withText(R.string.login_server_title))) .check(matches(withText(R.string.login_server_title)))

View file

@ -158,7 +158,7 @@ abstract class VerificationTestBase {
.createAccount(userName, password, null, it) .createAccount(userName, password, null, it)
} }
// Preform dummy step // Perform dummy step
val registrationResult = doSync<RegistrationResult> { val registrationResult = doSync<RegistrationResult> {
matrix.authenticationService() matrix.authenticationService()
.getRegistrationWizard() .getRegistrationWizard()

View file

@ -55,6 +55,10 @@ fun isAirplaneModeOn(context: Context): Boolean {
return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 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 * display the system dialog for granting this permission. If previously granted, the
* system will not show it (so you should call this method). * system will not show it (so you should call this method).

View file

@ -20,7 +20,6 @@ import android.app.Activity
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.Alerter
@ -28,6 +27,7 @@ import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy import dagger.Lazy
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity 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.home.AvatarRenderer
import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinActivity
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -173,9 +173,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) { private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
clearLightStatusBar() clearLightStatusBar()
val systemAnimationDurationDisabled = Settings.Global.getFloat( val noAnimation = !animate || isAnimationDisabled(activity)
activity.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
alert.weakCurrentActivity = WeakReference(activity) alert.weakCurrentActivity = WeakReference(activity)
val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout) val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout)
@ -192,7 +190,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
} }
} }
.apply { .apply {
if (systemAnimationDurationDisabled || !animate) { if (noAnimation) {
setEnterAnimation(R.anim.anim_alerter_no_anim) setEnterAnimation(R.anim.anim_alerter_no_anim)
} }
@ -242,7 +240,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color) setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color)
} }
} }
.enableIconPulse(!systemAnimationDurationDisabled) .enableIconPulse(!noAnimation)
.show() .show()
} }