diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt deleted file mode 100644 index bb9d23f449..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ -package com.flowcrypt.email - -import org.junit.runner.Description -import org.junit.runner.Runner -import org.junit.runner.manipulation.Filter -import org.junit.runner.manipulation.Filterable -import org.junit.runner.manipulation.NoTestsRemainException -import org.junit.runner.manipulation.Sortable -import org.junit.runner.manipulation.Sorter -import org.junit.runner.notification.RunNotifier -import org.junit.runners.model.InitializationError -import java.lang.reflect.Constructor -import java.lang.reflect.InvocationTargetException -import java.util.Locale - -/** - * Based on [androidx.test.ext.junit.runners.AndroidJUnit4] - * - * @author Denys Bondarenko - */ -class CustomAndroidJUnit4(klass: Class<*>) : Runner(), Filterable, Sortable { - private val delegate: Runner = loadRunner(klass) - - override fun getDescription(): Description { - return delegate.description - } - - override fun run(runNotifier: RunNotifier) { - delegate.run(runNotifier) - } - - @Throws(NoTestsRemainException::class) - override fun filter(filter: Filter) { - (delegate as Filterable).filter(filter) - } - - override fun sort(sorter: Sorter) { - (delegate as Sortable).sort(sorter) - } - - companion object { - private const val TAG = "AndroidJUnit4" - - private val runnerClassName: String - get() { - val runnerClassName = - System.getProperty("android.junit.runner", null) - ?: return if ( - System.getProperty("java.runtime.name") - ?.lowercase(Locale.getDefault())?.contains("android") == false - && hasClass("org.robolectric.RobolectricTestRunner") - ) { - "org.robolectric.RobolectricTestRunner" - } else { - "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner" - } - return runnerClassName - } - - private fun hasClass(className: String): Boolean { - return try { - Class.forName(className) != null - } catch (e: ClassNotFoundException) { - false - } - } - - @Throws(InitializationError::class) - private fun loadRunner(testClass: Class<*>): Runner { - val runnerClassName = runnerClassName - return loadRunner(testClass, runnerClassName) - } - - @Throws(InitializationError::class) - private fun loadRunner(testClass: Class<*>, runnerClassName: String): Runner { - var runnerClass: Class? = null - try { - runnerClass = Class.forName(runnerClassName) as Class - } catch (e: ClassNotFoundException) { - throwInitializationError( - String.format( - "Delegate runner %s for AndroidJUnit4 could not be found.\n", runnerClassName - ), - e - ) - } - - var constructor: Constructor? = null - try { - constructor = runnerClass!!.getConstructor(Class::class.java) - } catch (e: NoSuchMethodException) { - throwInitializationError( - String.format( - "Delegate runner %s for AndroidJUnit4 requires a public constructor that takes a" - + " Class.\n", - runnerClassName - ), - e - ) - } - - try { - return constructor!!.newInstance(testClass)!! - } catch (e: IllegalAccessException) { - throwInitializationError( - String.format("Illegal constructor access for test runner %s\n", runnerClassName), e - ) - } catch (e: InstantiationException) { - throwInitializationError( - String.format("Failed to instantiate test runner %s\n", runnerClassName), e - ) - } catch (e: InvocationTargetException) { - val details = getInitializationErrorDetails(e, testClass) - throwInitializationError( - String.format("Failed to instantiate test runner %s\n%s\n", runnerClass, details), e - ) - } - throw IllegalStateException("Should never reach here") - } - - @Throws(InitializationError::class) - private fun throwInitializationError(details: String, cause: Throwable) { - throw InitializationError(RuntimeException(details, cause)) - } - - private fun getInitializationErrorDetails(throwable: Throwable, testClass: Class<*>): String { - val innerCause = StringBuilder() - val cause = throwable.cause ?: return "" - - val causeClass: Class = cause.javaClass - if (causeClass == InitializationError::class.java) { - val initializationError = cause as InitializationError - val testClassProblemList = initializationError.causes - innerCause.append( - String.format( - "Test class %s is malformed. (%s problems):\n", - testClass, testClassProblemList.size - ) - ) - for (testClassProblem in testClassProblemList) { - innerCause.append(testClassProblem).append("\n") - } - } - return innerCause.toString() - } - } -} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/runner/RepeatableAndroidJUnit4ClassRunner.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/runner/RepeatableAndroidJUnit4ClassRunner.kt new file mode 100644 index 0000000000..1b980c6258 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/runner/RepeatableAndroidJUnit4ClassRunner.kt @@ -0,0 +1,62 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.runner + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.runner.Runner +import org.junit.runners.Suite +import org.junit.runners.model.FrameworkMethod + +/** + * A custom implementation of [Suite] which allows to run a test a few times naturally + * like [androidx.test.ext.junit.runners.AndroidJUnit4] does it. + * It helps debug issues with flaky tests when [com.flowcrypt.email.rules.RepeatRule] doesn't help. + * + * Inspired by [ParameterizedRobolectricTestRunner](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/ParameterizedRobolectricTestRunner.java) + * + * @author Denys Bondarenko + */ +class RepeatableAndroidJUnit4ClassRunner(klass: Class<*>?) : Suite(klass, emptyList()) { + private val runners = ArrayList() + + init { + val attemptsCount = + testClass.getAnnotation(RepeatTest::class.java)?.value?.takeIf { it > 0 } ?: 1 + for (i in 0 until attemptsCount) { + runners.add( + CustomAndroidJUnit4ClassRunner( + klass = testClass.javaClass, + postfix = "_${i + 1}".takeIf { attemptsCount > 1 } ?: "" + ) + ) + } + } + + override fun getChildren(): MutableList = runners + + /** + * Annotation for a class which provides attempts count + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.CLASS) + annotation class RepeatTest( + /** + * Should be a positive number + */ + val value: Int = 1 + ) + + /** + * A custom implementation of [AndroidJUnit4ClassRunner] which provides custom names for tests + * based on the given postfix. + */ + private class CustomAndroidJUnit4ClassRunner( + klass: Class<*>, private val postfix: String + ) : AndroidJUnit4ClassRunner(klass) { + override fun testName(method: FrameworkMethod) = method.name + postfix + override fun toString() = "TestClassRunnerForParameters_$postfix" + } +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/matchers/TextInputLayoutErrorMatcher.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/matchers/TextInputLayoutErrorMatcher.kt index f682c181f3..9b36c49d69 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/matchers/TextInputLayoutErrorMatcher.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/matchers/TextInputLayoutErrorMatcher.kt @@ -8,18 +8,24 @@ package com.flowcrypt.email.matchers import android.view.View import com.google.android.material.textfield.TextInputLayout import org.hamcrest.Description -import org.hamcrest.TypeSafeMatcher +import org.hamcrest.TypeSafeDiagnosingMatcher /** * @author Denys Bondarenko */ -class TextInputLayoutErrorMatcher(private val expectedHint: String) : TypeSafeMatcher() { +class TextInputLayoutErrorMatcher(private val expectedHint: String) : + TypeSafeDiagnosingMatcher() { override fun describeTo(description: Description) { description.appendText("TextInputLayout with error = \"$expectedHint\"") } - override fun matchesSafely(view: View): Boolean { - return expectedHint == (view as? TextInputLayout)?.error?.toString() + override fun matchesSafely(item: View?, mismatchDescription: Description?): Boolean { + val actualHint = (item as? TextInputLayout)?.error?.toString() + return (expectedHint == actualHint).apply { + if (!this && mismatchDescription !is Description.NullDescription) { + mismatchDescription?.appendText("Actual was: $actualHint") + } + } } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt index c835abfc9d..9172ba221b 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -59,6 +59,8 @@ class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { .around(ScreenshotTestRule()) @Test + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testShowWarningIfAuthFail() { enableAdvancedMode() val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt index 1c562157a6..e0d751fba0 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -181,6 +181,8 @@ class ComposeScreenDraftGmailAPIFlowTest : BaseComposeScreenTest() { .around(ScreenshotTestRule()) @Test + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testSavingDraftViaGmailAPI() { activeActivityRule?.launch(intent) registerAllIdlingResources() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenFlowTest.kt index a40f54e5e3..481e6695db 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenFlowTest.kt @@ -17,6 +17,7 @@ import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.doesNotExist @@ -97,7 +98,6 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class ComposeScreenFlowTest : BaseComposeScreenTest() { private val addPrivateKeyToDatabaseRule = AddPrivateKeyToDatabaseRule() - private val temporaryFolderRule = TemporaryFolder.builder().parentFolder(SHARED_FOLDER).build() @get:Rule var ruleChain: TestRule = RuleChain @@ -106,7 +106,6 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { .around(GrantPermissionRuleChooser.grant(android.Manifest.permission.POST_NOTIFICATIONS)) .around(addAccountToDatabaseRule) .around(addPrivateKeyToDatabaseRule) - .around(temporaryFolderRule) .around(activeActivityRule) .around(ScreenshotTestRule()) @@ -163,24 +162,32 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { } @Test + //@Ignore("flaky 5") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testEmptyEmailMsg() { activeActivityRule?.launch(intent) - registerAllIdlingResources() onView(withId(R.id.editTextEmailAddress)) .perform( - typeText(TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER), + replaceText(TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER), pressImeActionButton() ) onView(withId(R.id.editTextEmailSubject)) .check(matches(isDisplayed())) - .perform(scrollTo(), click(), typeText(EMAIL_SUBJECT)) + .perform(scrollTo(), click(), replaceText(EMAIL_SUBJECT)) onView(withId(R.id.editTextEmailMessage)) - .perform(scrollTo()) + .perform(scrollTo(), click(), replaceText("")) .check(matches(withText(`is`(emptyString())))) + Espresso.closeSoftKeyboard() onView(withId(R.id.menuActionSend)) .check(matches(isDisplayed())) .perform(click()) + + waitForObjectWithText( + getResString(R.string.your_message_must_be_non_empty), + TimeUnit.SECONDS.toMillis(10) + ) + onView(withText(getResString(R.string.your_message_must_be_non_empty))) .check(matches(isDisplayed())) } @@ -327,6 +334,8 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { } @Test + //@Ignore("flaky 4") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testDeletingAtts() { activeActivityRule?.launch(intent) waitForObjectWithText( @@ -544,7 +553,7 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { TimeUnit.SECONDS.toMillis(10) ) - onView(withText(att?.name)) + onView(withText(att.name)) .check(matches(isDisplayed())) } @@ -610,7 +619,7 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { TimeUnit.SECONDS.toMillis(10) ) - onView(withText(att?.name)) + onView(withText(att.name)) .check(matches(isDisplayed())) } @@ -907,7 +916,8 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { @get:ClassRule @JvmStatic - val temporaryFolderRule = TemporaryFolder.builder().parentFolder(SHARED_FOLDER).build() + val temporaryFolderRule: TemporaryFolder = + TemporaryFolder.builder().parentFolder(SHARED_FOLDER).build() @get:ClassRule @JvmStatic diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt index 6a5473d904..d008a1117d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -90,6 +90,8 @@ class ComposeScreenImportRecipientPubKeyFlowTest : BaseComposeScreenTest() { } @Test + //@Ignore("flaky 7") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testImportRecipientPubKeyFromClipboard() { fillDataAndMoveToImportPublicKeyScreen() addTextToClipboard("public key", publicKey) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt index 4980405d86..cda2f403ff 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -31,6 +31,7 @@ import org.junit.Test import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit /** * @author Denys Bondarenko @@ -62,15 +63,17 @@ class ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest : .around(ScreenshotTestRule()) @Test + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testAddEmailToExistingKey() { doTestAddEmailToExistingKey { - waitForObjectWithText(getResString(android.R.string.ok), 2000) + waitForObjectWithText(getResString(android.R.string.ok), TimeUnit.SECONDS.toMillis(2)) onView(withId(R.id.buttonOk)) .check(matches(isDisplayed())) .perform(click()) - waitForObjectWithText(getResString(R.string.provide_passphrase), 2000) + waitForObjectWithText(getResString(R.string.provide_passphrase), TimeUnit.SECONDS.toMillis(2)) onView(withId(R.id.eTKeyPassword)) .inRoot(RootMatchers.isDialog()) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt index 19bf96f912..e865eeebc7 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -28,7 +28,6 @@ import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest import com.flowcrypt.email.util.PrivateKeysManager -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -86,6 +85,8 @@ class ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest : Bas } @Test + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testAddEmailToExistingSingleKeyPassphraseInDatabase() { doTestAddEmailToExistingKey { //no more additional actions diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt index 54d691dfd7..0c19d03935 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -134,6 +134,8 @@ class ImportRecipientsFromSourceFragmentFlowTest : BaseTest() { } @Test + //@Ignore("flaky 4") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testFetchKeyFromAttesterForExistedUserImeAction() { onView(withId(R.id.eTKeyIdOrEmail)) .perform( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt index 2261f48457..bf116e187f 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui @@ -209,6 +209,8 @@ class MainSignInFragmentFlowTest : BaseSignTest() { } @Test + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testClientConfigurationCombinationNotSupportedForForbidCreatingPrivateKeyMissing() { setupAndClickSignInButton( genMockGoogleSignInAccountJson( @@ -226,6 +228,8 @@ class MainSignInFragmentFlowTest : BaseSignTest() { } @Test + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testErrorGetPrvKeysViaEkm() { setupAndClickSignInButton(genMockGoogleSignInAccountJson(EMAIL_GET_KEYS_VIA_EKM_ERROR)) isDialogWithTextDisplayed(decorView, EKM_ERROR) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt index 1542c1f885..c4b5bc4ea8 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt @@ -98,6 +98,8 @@ class MessageDetailsChangeGmailLabelsFlowTest : BaseGmailLabelsFlowTest() { .around(ScreenshotTestRule()) @Test + //@Ignore("flaky 1") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testLabelsManagement() { val allLabels = initLabelIds() lastLabelIds = (allLabels.take(4) + allLabels.takeLast(1)).toMutableList() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt index ada4d3077a..7e5eff780d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt @@ -129,23 +129,16 @@ class MessageDetailsFlowTest : BaseMessageDetailsFlowTest() { } @Test - fun testReplyButton() { - val incomingMessageInfo = testStandardMsgPlaintextInternal() - onView(withId(R.id.replyButton)) - .check(matches(isDisplayed())) - .perform(scrollTo(), click()) - intended(hasComponent(CreateMessageActivity::class.java.name)) - - checkQuotesFunctionality(incomingMessageInfo) - } - - @Test + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testTopReplyButton() { val incomingMessageInfo = testTopReplyAction(getResString(R.string.reply)) checkQuotesFunctionality(incomingMessageInfo) } @Test + //@Ignore("flaky 2 3") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testReplyAllButton() { val incomingMessageInfo = testStandardMsgPlaintextInternal() onView(withId(R.id.replyAllButton)) @@ -1337,8 +1330,20 @@ class MessageDetailsFlowTest : BaseMessageDetailsFlowTest() { .check(matches(isDisplayed())) .perform(scrollTo(), click()) + val replyContent = EmailUtil.genReplyContent(incomingMessageInfo) + + waitForObjectWithText( + "Re: ${incomingMessageInfo?.msgEntity?.subject}", + TimeUnit.SECONDS.toMillis(10) + ) + + waitForObjectWithText( + replyContent, + TimeUnit.SECONDS.toMillis(10) + ) + onView(withId(R.id.editTextEmailMessage)) .check(matches(isDisplayed())) - .check(matches(withText(EmailUtil.genReplyContent(incomingMessageInfo)))) + .check(matches(withText(replyContent))) } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt index 88ecc4eebf..3c55fa43b6 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt @@ -49,7 +49,6 @@ import com.flowcrypt.email.util.TestGeneralUtil import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -175,7 +174,7 @@ class UpdatePrivateKeyWithPassPhraseInRamFlowTest : BaseTest() { //type key onView(withId(R.id.editTextNewPrivateKey)) .check(matches(isDisplayed())) - .perform(replaceText(updatedKeyDetails.privateKey?:""), closeSoftKeyboard()) + .perform(replaceText(updatedKeyDetails.privateKey ?: ""), closeSoftKeyboard()) onView(withId(R.id.buttonCheck)) .check(matches(isDisplayed())) @@ -216,6 +215,8 @@ class UpdatePrivateKeyWithPassPhraseInRamFlowTest : BaseTest() { } @Test + //@Ignore("flaky 2 2") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testPrivateKeyPassphraseAntiBruteforceProtection() { onView(withText(R.string.pass_phrase_not_provided)) .check(matches(isDisplayed())) @@ -225,50 +226,40 @@ class UpdatePrivateKeyWithPassPhraseInRamFlowTest : BaseTest() { .perform(click()) val wrongPassphrase = "wrong pass phrase" + //show typed passphrase for debug + onView(withId(com.google.android.material.R.id.text_input_end_icon)) + .perform(click()) + + val attemptsMaxValue = AccountSettingsEntity.ANTI_BRUTE_FORCE_PROTECTION_ATTEMPTS_MAX_VALUE - for (i in 0 until AccountSettingsEntity.ANTI_BRUTE_FORCE_PROTECTION_ATTEMPTS_MAX_VALUE) { + for (i in 0 until attemptsMaxValue) { + val attemptsLeft = attemptsMaxValue - i - 1 onView(withId(R.id.eTKeyPassword)) .perform( clearText(), - replaceText(wrongPassphrase), + replaceText("$wrongPassphrase: left $attemptsLeft attempts"), closeSoftKeyboard() ) onView(withId(R.id.btnUpdatePassphrase)) .perform(click()) - Thread.sleep(2000) - if (i == AccountSettingsEntity.ANTI_BRUTE_FORCE_PROTECTION_ATTEMPTS_MAX_VALUE - 1) { - onView(withId(R.id.tILKeyPassword)) - .check( - matches( - withTextInputLayoutError( - getResString( - R.string.private_key_passphrase_anti_bruteforce_protection_hint - ) - ) - ) - ) + + val text = if (i == attemptsMaxValue - 1) { + getResString(R.string.private_key_passphrase_anti_bruteforce_protection_hint) } else { - val attemptsLeft = - AccountSettingsEntity.ANTI_BRUTE_FORCE_PROTECTION_ATTEMPTS_MAX_VALUE - i - 1 - - onView(withId(R.id.tILKeyPassword)) - .check( - matches( - withTextInputLayoutError( - getResString(R.string.password_is_incorrect) + - "\n\n" + - getQuantityString( - R.plurals.next_attempt_warning_about_wrong_pass_phrase, - attemptsLeft, - attemptsLeft - ) - ) + getResString(R.string.password_is_incorrect) + + "\n\n" + + getQuantityString( + R.plurals.next_attempt_warning_about_wrong_pass_phrase, + attemptsLeft, + attemptsLeft ) - ) } + waitForObjectWithText(text, TimeUnit.SECONDS.toMillis(20)) + + onView(withTextInputLayoutError(text)) + .check(matches(isDisplayed())) checkPassPhraseAttemptsCount(i + 1) - Thread.sleep(1000) } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fragment/isolation/incontainer/LegalSettingsFragmentInIsolationTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fragment/isolation/incontainer/LegalSettingsFragmentInIsolationTest.kt index fdee350bdf..00775aa275 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fragment/isolation/incontainer/LegalSettingsFragmentInIsolationTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fragment/isolation/incontainer/LegalSettingsFragmentInIsolationTest.kt @@ -1,13 +1,11 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.fragment.isolation.incontainer import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.swipeLeft import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isSelected @@ -15,17 +13,16 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.FlakyTest import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.base.BaseTest -import com.flowcrypt.email.junit.annotations.NotReadyForCI import com.flowcrypt.email.rules.AddAccountToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.activity.fragment.LegalSettingsFragment +import com.flowcrypt.email.viewaction.ViewPager2Actions.scrollRight import org.hamcrest.Matchers.allOf import org.junit.Before import org.junit.Rule @@ -60,19 +57,6 @@ class LegalSettingsFragmentInIsolationTest : BaseTest() { launchFragmentInContainer() } - @Test - @FlakyTest - @NotReadyForCI - fun testClickToTitleViewPager() { - for (titleName in titleNames) { - onView(allOf(withParent(withParent(withParent(withId(R.id.tabLayout)))), withText(titleName))) - .check(matches(isDisplayed())) - .perform(click()) - onView(allOf(withParent(withParent(withParent(withId(R.id.tabLayout)))), withText(titleName))) - .check(matches(isDisplayed())).check(matches(isSelected())) - } - } - @Test fun testSwipeInViewPager() { onView( @@ -83,7 +67,7 @@ class LegalSettingsFragmentInIsolationTest : BaseTest() { ) .check(matches(isDisplayed())).check(matches(isSelected())) for (i in 1 until titleNames.size) { - onView(withId(R.id.viewPager2)).perform(swipeLeft()) + onView(withId(R.id.viewPager2)).perform(scrollRight()) onView( allOf( withParent(withParent(withParent(withId(R.id.tabLayout)))), diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ComposeScreenForwardWithGmailApiSignatureFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ComposeScreenForwardWithGmailApiSignatureFlowTest.kt index 04e4dd5cb8..cf6a5fbb7d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ComposeScreenForwardWithGmailApiSignatureFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ComposeScreenForwardWithGmailApiSignatureFlowTest.kt @@ -32,6 +32,7 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.CoreMatchers.startsWith +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -72,6 +73,7 @@ class ComposeScreenForwardWithGmailApiSignatureFlowTest : .around(ScreenshotTestRule()) @Test + @Ignore("flaky. It looks like adding signature after start has unexpected result for forwarding case") fun testAddingSignatureAfterStart() { //need to wait while the app loads the messages list Thread.sleep(2000) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt index fc316d5abd..0fb372b508 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt @@ -84,9 +84,11 @@ class ThreadsListGmailApiFlowTest : BaseGmailApiTest( .around(ScreenshotTestRule()) @Test + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testShowCorrectThreadsDetailsInList() { //need to wait while the app loads the thread list - waitForObjectWithText(SUBJECT_EXISTING_STANDARD, TimeUnit.SECONDS.toMillis(10)) + waitForObjectWithText(SUBJECT_EXISTING_STANDARD, TimeUnit.SECONDS.toMillis(20)) //test thread with 2 standard messages with attachments checkThreadRowDetails( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/passwordprotected/ComposeScreenPasswordProtectedDisallowedTermsFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/passwordprotected/ComposeScreenPasswordProtectedDisallowedTermsFlowTest.kt index e10df8a527..6b475e8767 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/passwordprotected/ComposeScreenPasswordProtectedDisallowedTermsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/gmailapi/passwordprotected/ComposeScreenPasswordProtectedDisallowedTermsFlowTest.kt @@ -70,6 +70,8 @@ class ComposeScreenPasswordProtectedDisallowedTermsFlowTest : .around(ScreenshotTestRule()) @Test + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testDialogWithErrorText() { intentsRelease() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/viewaction/ViewPager2Actions.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/viewaction/ViewPager2Actions.kt new file mode 100644 index 0000000000..c541befaad --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/viewaction/ViewPager2Actions.kt @@ -0,0 +1,140 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ +package com.flowcrypt.email.viewaction + +import android.view.View +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers +import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import org.hamcrest.Matcher + +/** + * Based on [androidx.test.espresso.contrib.ViewPagerActions] + * + * @author Denys Bondarenko + */ +object ViewPager2Actions { + private const val DEFAULT_SMOOTH_SCROLL = false + + fun scrollRight(smoothScroll: Boolean = DEFAULT_SMOOTH_SCROLL): ViewAction { + return object : ViewPager2ScrollAction() { + override fun getDescription() = "ViewPager2 move one page to the right" + override fun performScroll(viewPager2: ViewPager2?) { + val current = viewPager2?.currentItem ?: throw IllegalStateException() + viewPager2.setCurrentItem(current + 1, smoothScroll) + } + } + } + + fun scrollLeft(smoothScroll: Boolean = DEFAULT_SMOOTH_SCROLL): ViewAction { + return object : ViewPager2ScrollAction() { + override fun getDescription() = "ViewPager2 move one page to the left" + override fun performScroll(viewPager2: ViewPager2?) { + val current = viewPager2?.currentItem ?: throw IllegalStateException() + viewPager2.setCurrentItem(current - 1, smoothScroll) + } + } + } + + fun scrollToLast(smoothScroll: Boolean = DEFAULT_SMOOTH_SCROLL): ViewAction { + return object : ViewPager2ScrollAction() { + override fun getDescription() = "ViewPager2 move to last page" + override fun performScroll(viewPager2: ViewPager2?) { + val size = viewPager2?.adapter?.itemCount ?: throw IllegalStateException() + if (size > 0) { + viewPager2.setCurrentItem(size - 1, smoothScroll) + } + } + } + } + + fun scrollToFirst(smoothScroll: Boolean = DEFAULT_SMOOTH_SCROLL): ViewAction { + return object : ViewPager2ScrollAction() { + override fun getDescription() = "ViewPager2 move to first page" + override fun performScroll(viewPager2: ViewPager2?) { + val size = viewPager2?.adapter?.itemCount ?: throw IllegalStateException() + if (size > 0) { + viewPager2.setCurrentItem(0, smoothScroll) + } + } + } + } + + fun scrollToPage(page: Int, smoothScroll: Boolean = DEFAULT_SMOOTH_SCROLL): ViewAction { + return object : ViewPager2ScrollAction() { + override fun getDescription() = "ViewPager2 move to page" + override fun performScroll(viewPager2: ViewPager2?) { + viewPager2?.setCurrentItem(page, smoothScroll) + } + } + } + + private class CustomViewPager2Listener : OnPageChangeCallback(), IdlingResource { + private var currentState = ViewPager2.SCROLL_STATE_IDLE + private var idlingResourceResourceCallback: IdlingResource.ResourceCallback? = null + var needsIdle = false + + override fun registerIdleTransitionCallback( + resourceCallback: IdlingResource.ResourceCallback? + ) { + idlingResourceResourceCallback = resourceCallback + } + + override fun getName() = "View pager listener" + + override fun isIdleNow(): Boolean { + return if (!needsIdle) { + true + } else { + currentState == ViewPager2.SCROLL_STATE_IDLE + } + } + + override fun onPageSelected(position: Int) { + if (currentState == ViewPager2.SCROLL_STATE_IDLE) { + idlingResourceResourceCallback?.onTransitionToIdle() + } + } + + override fun onPageScrollStateChanged(state: Int) { + currentState = state + if (currentState == ViewPager2.SCROLL_STATE_IDLE) { + idlingResourceResourceCallback?.onTransitionToIdle() + } + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} + } + + abstract class ViewPager2ScrollAction : ViewAction { + override fun getConstraints(): Matcher = ViewMatchers.isDisplayed() + + override fun perform(uiController: UiController, view: View?) { + (view as? ViewPager2)?.apply { + val customListener = CustomViewPager2Listener() + registerOnPageChangeCallback(customListener) + try { + IdlingRegistry.getInstance().register(customListener) + uiController.loopMainThreadUntilIdle() + performScroll(this) + uiController.loopMainThreadUntilIdle() + customListener.needsIdle = true + uiController.loopMainThreadUntilIdle() + customListener.needsIdle = false + } finally { + IdlingRegistry.getInstance().unregister(customListener) + unregisterOnPageChangeCallback(customListener) + } + } + } + + protected abstract fun performScroll(viewPager2: ViewPager2?) + } +} + diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt index 0a3c02376f..034b2edaa0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt @@ -505,7 +505,24 @@ class CreateMessageFragment : BaseFragment(), if (composeMsgViewModel.msgEncryptionType === MessageEncryptionType.ENCRYPTED) { val adapter = parent.adapter as ArrayAdapter<*> val colorGray = UIUtil.getColor(requireContext(), R.color.gray) - binding?.editTextFrom?.setTextColor(if (adapter.isEnabled(position)) originalColor else colorGray) + + if ( + adapter is FromAddressesAdapter + && !adapter.hasAnyKeysAvailabilityRecords() + && KeysStorageImpl.getInstance(requireContext()) + .secretKeyRingsLiveData.value?.isNotEmpty() == true + ) { + updateFromAddressAdapter( + KeysStorageImpl.getInstance(requireContext()).getPGPSecretKeyRings() + ) + } + binding?.editTextFrom?.setTextColor( + if (adapter.isEnabled(position)) { + originalColor + } else { + colorGray + } + ) } else { binding?.editTextFrom?.setTextColor(originalColor) } @@ -2163,11 +2180,12 @@ class CreateMessageFragment : BaseFragment(), return } - val messageHasOldSignature = oldSignature != null && binding?.editTextEmailMessage?.text?.contains( - ("^$oldSignature$").toRegex(RegexOption.MULTILINE) - ) == true + val messageHasOldSignature = + oldSignature != null && binding?.editTextEmailMessage?.text?.contains( + ("^$oldSignature$").toRegex(RegexOption.MULTILINE) + ) == true - if (messageHasOldSignature && oldSignature != null) { + if (messageHasOldSignature) { useNewSignature = true binding?.editTextEmailMessage?.setText( binding?.editTextEmailMessage?.text?.replaceFirst( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FromAddressesAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FromAddressesAdapter.kt index 9d2ba165a9..6089f834f6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FromAddressesAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FromAddressesAdapter.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.adapter @@ -94,4 +94,6 @@ class FromAddressesAdapter( val result = keysAvailability[emailAddress.lowercase()] return keysAvailability.containsKey(emailAddress.lowercase()) && result != null && result } + + fun hasAnyKeysAvailabilityRecords(): Boolean = keysAvailability.isNotEmpty() } diff --git a/script/run_tests_in_loop.sh b/script/run_tests_in_loop.sh new file mode 100755 index 0000000000..84951b641f --- /dev/null +++ b/script/run_tests_in_loop.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com +# Contributors: denbond7 +# + +set -o xtrace + +attempts=$1 +script_name=$2 + +if [[ -z attempts ]]; then + echo "Please specify attempts count" + exit 1 +else + if [[ $attempts =~ ^[\-0-9]+$ ]] && ((attempts > 0)); then + echo "We are going to run tests for $attempts iterations" + echo "------------------------------------------------------" + else + echo "Please specify attempts count as a number > 0" + exit 1 + fi +fi + +for ((i=1;i<=attempts;i++)); do + echo "Run attempt $i" + ./script/adb_kill_all_emualtor.sh + sleep 60 + "$ANDROID_HOME/emulator/emulator" -avd ci-emulator -no-snapshot -no-window -no-boot-anim -no-audio -gpu auto -read-only -no-metrics & + ./script/ci-wait-for-emulator.sh + ./script/$script_name + testResults=$? + if [[ $testResults -ne 0 ]]; then + echo "Attempt $i failed" + echo "Stopping..." + exit 1 + else + echo "------------------------------------------------------" + fi +done + +echo "Tests completed for $attempts iterations"