mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 03:48:12 +03:00
PR Review
Cleanup and Add command line to run the UI tests
This commit is contained in:
parent
f79784bc8c
commit
b14d22550b
10 changed files with 75 additions and 50 deletions
|
@ -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)
|
||||
===================================================
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
`````
|
||||
```
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
|
@ -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)))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue