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:
-
- 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)
===================================================

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:
```
```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)
}
`````
```

View file

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

View file

@ -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<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk

View file

@ -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'

View file

@ -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<View>, timeout: Long = 10000, waitForDisplayed: Boolean = true): ViewAction {
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
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) {
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<PrivateKeysInfo>?) {
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)

View file

@ -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)))

View file

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

View file

@ -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<Ava
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {
clearLightStatusBar()
val systemAnimationDurationDisabled = Settings.Global.getFloat(
activity.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
val noAnimation = !animate || isAnimationDisabled(activity)
alert.weakCurrentActivity = WeakReference(activity)
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 {
if (systemAnimationDurationDisabled || !animate) {
if (noAnimation) {
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)
}
}
.enableIconPulse(!systemAnimationDurationDisabled)
.enableIconPulse(!noAnimation)
.show()
}