From 44fed53ac6cf5354778bbde4a9bb59abeb0f25cf Mon Sep 17 00:00:00 2001 From: denbond7 Date: Fri, 7 Feb 2025 11:06:08 +0200 Subject: [PATCH 01/25] emulator 0.| #2914 --- .../email/ui/AddOtherAccountFlowTest.kt | 4 +- .../ui/ComposeScreenDraftGmailAPIFlowTest.kt | 4 +- .../email/ui/ComposeScreenFlowTest.kt | 3 ++ ...poseScreenImportRecipientPubKeyFlowTest.kt | 4 +- ...MultipleKeysWithPassphraseInRamFlowTest.kt | 4 +- ...ngleKeyWithPassphraseInDatabaseFlowTest.kt | 3 +- ...ortRecipientsFromSourceFragmentFlowTest.kt | 4 +- .../email/ui/MainSignInFragmentFlowTest.kt | 5 ++- ...MessageDetailsChangeGmailLabelsFlowTest.kt | 2 + .../email/ui/MessageDetailsFlowTest.kt | 2 + .../LegalSettingsFragmentInIsolationTest.kt | 4 +- ...eenForwardWithGmailApiSignatureFlowTest.kt | 2 + .../gmailapi/ThreadsListGmailApiFlowTest.kt | 2 + ...asswordProtectedDisallowedTermsFlowTest.kt | 2 + script/run_tests_in_loop.sh | 41 +++++++++++++++++++ 15 files changed, 78 insertions(+), 8 deletions(-) create mode 100755 script/run_tests_in_loop.sh 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..3d5f235e5d 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 @@ -31,6 +31,7 @@ import com.flowcrypt.email.util.AuthCredentialsManager import com.flowcrypt.email.util.TestGeneralUtil import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.startsWith +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -59,6 +60,7 @@ class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { .around(ScreenshotTestRule()) @Test + @Ignore("flaky 8") 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..95e9ac4239 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 @@ -42,6 +42,7 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -181,6 +182,7 @@ class ComposeScreenDraftGmailAPIFlowTest : BaseComposeScreenTest() { .around(ScreenshotTestRule()) @Test + @Ignore("flaky 8") 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..09b75e950b 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenFlowTest.kt @@ -78,6 +78,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.BeforeClass import org.junit.ClassRule +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -163,6 +164,7 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { } @Test + @Ignore("flaky 5") fun testEmptyEmailMsg() { activeActivityRule?.launch(intent) registerAllIdlingResources() @@ -327,6 +329,7 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { } @Test + @Ignore("flaky 4") fun testDeletingAtts() { activeActivityRule?.launch(intent) waitForObjectWithText( 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..2e397c115e 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 @@ -32,6 +32,7 @@ import com.flowcrypt.email.ui.adapter.RecipientChipRecyclerViewAdapter import com.flowcrypt.email.ui.base.BaseComposeScreenTest import com.flowcrypt.email.util.TestGeneralUtil import org.hamcrest.Matchers.allOf +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -90,6 +91,7 @@ class ComposeScreenImportRecipientPubKeyFlowTest : BaseComposeScreenTest() { } @Test + @Ignore("flaky 7") 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..aa484799c4 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 @@ -26,6 +26,7 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -62,6 +63,7 @@ class ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest : .around(ScreenshotTestRule()) @Test + @Ignore("flaky") fun testAddEmailToExistingKey() { doTestAddEmailToExistingKey { waitForObjectWithText(getResString(android.R.string.ok), 2000) 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..ef176c16e2 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 @@ -86,6 +86,7 @@ class ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest : Bas } @Test + @Ignore("flaky") 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..223e0666a3 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 @@ -36,6 +36,7 @@ import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.CoreMatchers.containsString import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -134,6 +135,7 @@ class ImportRecipientsFromSourceFragmentFlowTest : BaseTest() { } @Test + @Ignore("flaky 4") 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..b2ad72effe 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 @@ -50,6 +50,7 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.not +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -209,6 +210,7 @@ class MainSignInFragmentFlowTest : BaseSignTest() { } @Test + @Ignore("flaky 8") fun testClientConfigurationCombinationNotSupportedForForbidCreatingPrivateKeyMissing() { setupAndClickSignInButton( genMockGoogleSignInAccountJson( @@ -226,6 +228,7 @@ class MainSignInFragmentFlowTest : BaseSignTest() { } @Test + @Ignore("flaky 8") 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..fe5c90f3ed 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt @@ -34,6 +34,7 @@ import okhttp3.mockwebserver.RecordedRequest import okio.GzipSource import okio.buffer import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -98,6 +99,7 @@ class MessageDetailsChangeGmailLabelsFlowTest : BaseGmailLabelsFlowTest() { .around(ScreenshotTestRule()) @Test + @Ignore("flaky 1") 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 e444a60076..1a71e83959 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt @@ -84,6 +84,7 @@ import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -139,6 +140,7 @@ class MessageDetailsFlowTest : BaseMessageDetailsFlowTest() { } @Test + @Ignore("flaky") fun testTopReplyButton() { val incomingMessageInfo = testTopReplyAction(getResString(R.string.reply)) checkQuotesFunctionality(incomingMessageInfo) 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..4632251239 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,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.fragment.isolation.incontainer @@ -28,6 +28,7 @@ import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.activity.fragment.LegalSettingsFragment import org.hamcrest.Matchers.allOf import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -74,6 +75,7 @@ class LegalSettingsFragmentInIsolationTest : BaseTest() { } @Test + @Ignore("flaky") fun testSwipeInViewPager() { onView( allOf( 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..a003e0f601 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") 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 3d1d87e834..e8c02325c2 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 @@ -31,6 +31,7 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.allOf +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -84,6 +85,7 @@ class ThreadsListGmailApiFlowTest : BaseGmailApiTest( .around(ScreenshotTestRule()) @Test + @Ignore("flaky") fun testShowCorrectThreadsDetailsInList() { //need to wait while the app loads the thread list waitForObjectWithText(SUBJECT_EXISTING_STANDARD, TimeUnit.SECONDS.toMillis(10)) 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..a09a11764f 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 @@ -30,6 +30,7 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import org.hamcrest.Matchers.allOf +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -70,6 +71,7 @@ class ComposeScreenPasswordProtectedDisallowedTermsFlowTest : .around(ScreenshotTestRule()) @Test + @Ignore("flaky") fun testDialogWithErrorText() { intentsRelease() diff --git a/script/run_tests_in_loop.sh b/script/run_tests_in_loop.sh new file mode 100755 index 0000000000..8be25a30ee --- /dev/null +++ b/script/run_tests_in_loop.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com +# Contributors: denbond7 +# + +set -o xtrace + +attempts=$1 + +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/ci-instrumentation-tests-without-mailserver.sh 4 2 + testResults=$? + if [[ $testResults -ne 0 ]]; then + echo "Attempt $i failed" + echo "Stopping..." + exit 1 + else + echo "------------------------------------------------------" + fi +done + +echo "Tests completed for $attempts iterations" From 3b94bed015c9c0b4fc47661fdfaa9c65b04525cf Mon Sep 17 00:00:00 2001 From: denbond7 Date: Fri, 7 Feb 2025 11:06:46 +0200 Subject: [PATCH 02/25] emulator 2.| #2914 --- .../email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt | 1 + 1 file changed, 1 insertion(+) 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..e583f42e07 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/UpdatePrivateKeyWithPassPhraseInRamFlowTest.kt @@ -216,6 +216,7 @@ class UpdatePrivateKeyWithPassPhraseInRamFlowTest : BaseTest() { } @Test + @Ignore("flaky 2 2") fun testPrivateKeyPassphraseAntiBruteforceProtection() { onView(withText(R.string.pass_phrase_not_provided)) .check(matches(isDisplayed())) From d5976170adbdcbbd5cfa716351292065f06c03a4 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Fri, 7 Feb 2025 11:07:16 +0200 Subject: [PATCH 03/25] emulator 3.| #2914 --- .../java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt | 1 + 1 file changed, 1 insertion(+) 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 1a71e83959..72ec56ac27 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt @@ -147,6 +147,7 @@ class MessageDetailsFlowTest : BaseMessageDetailsFlowTest() { } @Test + @Ignore("flaky 2 3") fun testReplyAllButton() { val incomingMessageInfo = testStandardMsgPlaintextInternal() onView(withId(R.id.replyAllButton)) From 93fd76e6fab6a47a8ed8410500896ec1e52e9de9 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Fri, 7 Feb 2025 11:11:04 +0200 Subject: [PATCH 04/25] wip --- script/run_tests_in_loop.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/run_tests_in_loop.sh b/script/run_tests_in_loop.sh index 8be25a30ee..84951b641f 100755 --- a/script/run_tests_in_loop.sh +++ b/script/run_tests_in_loop.sh @@ -7,6 +7,7 @@ set -o xtrace attempts=$1 +script_name=$2 if [[ -z attempts ]]; then echo "Please specify attempts count" @@ -27,7 +28,7 @@ for ((i=1;i<=attempts;i++)); do 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/ci-instrumentation-tests-without-mailserver.sh 4 2 + ./script/$script_name testResults=$? if [[ $testResults -ne 0 ]]; then echo "Attempt $i failed" From 28d0e740c88d82d167aee8b0d741cad0e5247d52 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Mar 2025 11:51:48 +0200 Subject: [PATCH 05/25] Added a template for CustomSuite. |#2946 --- .../flowcrypt/email/CustomAndroidJUnit4.kt | 4 +- .../email/junit/suites/ClassParameter.kt | 51 +++ .../com/flowcrypt/email/junit/suites/Clock.kt | 13 + .../email/junit/suites/CustomSuite.kt | 402 ++++++++++++++++++ .../email/junit/suites/InsideTraversal.kt | 13 + .../flowcrypt/email/junit/suites/Metadata.kt | 17 + .../flowcrypt/email/junit/suites/Metric.kt | 82 ++++ .../email/junit/suites/PerfStatsCollector.kt | 154 +++++++ .../email/ui/AddOtherAccountFlowTest.kt | 106 +---- 9 files changed, 752 insertions(+), 90 deletions(-) create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt index bb9d23f449..e6271e5dd5 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt @@ -22,8 +22,8 @@ import java.util.Locale * * @author Denys Bondarenko */ -class CustomAndroidJUnit4(klass: Class<*>) : Runner(), Filterable, Sortable { - private val delegate: Runner = loadRunner(klass) +open class CustomAndroidJUnit4(klass: Class<*>) : Runner(), Filterable, Sortable { + val delegate: Runner = loadRunner(klass) override fun getDescription(): Description { return delegate.description diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt new file mode 100644 index 0000000000..dbdb549306 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt @@ -0,0 +1,51 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.suites + +/** + * @author Denys Bondarenko + */ +/** + * Typed parameter used with reflective method calls. + * + * @param The value of the method parameter. + */ +class ClassParameter(val clazz: Class?, val value: V?) { + companion object { + fun from(clazz: Class?, value: V?): ClassParameter { + return ClassParameter(clazz, value) + } + + fun fromComponentLists( + classes: Array?>, + values: Array + ): Array?> { + val classParameters = arrayOfNulls>(classes.size) + for (i in classes.indices) { + classParameters[i] = from(classes[i], values[i]) + } + return classParameters + } + + fun getClasses(vararg classParameters: ClassParameter<*>?): Array?> { + val classes = arrayOfNulls>(classParameters.size) + for (i in classParameters.indices) { + val paramClass: Class<*>? = classParameters[i]!!.clazz + classes[i] = paramClass + } + return classes + } + + fun getValues(vararg classParameters: ClassParameter<*>?): Array { + val values = arrayOfNulls(classParameters.size) + for (i in classParameters.indices) { + val paramValue: Any? = classParameters[i]!!.value + values[i] = paramValue + } + return values + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt new file mode 100644 index 0000000000..399da337fb --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt @@ -0,0 +1,13 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.suites + +/** + * @author Denys Bondarenko + */ +internal fun interface Clock { + fun nanoTime(): Long +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt new file mode 100644 index 0000000000..c551a5eac7 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt @@ -0,0 +1,402 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.suites + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.Assert +import org.junit.runner.Runner +import org.junit.runners.Suite +import org.junit.runners.model.FrameworkField +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.TestClass +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.text.MessageFormat +import java.util.Arrays +import java.util.Locale + +/** + * @author Denys Bondarenko + */ +class CustomSuite(klass: Class<*>?) : Suite(klass, mutableListOf()) { + /** + * Annotation for a method which provides parameters to be injected into the test class + * constructor by `Parameterized` + */ + @Retention(AnnotationRetention.RUNTIME) + @Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER + ) + annotation class Parameters( + /** + * Optional pattern to derive the test's name from the parameters. Use numbers in braces to + * refer to the parameters or the additional data as follows: + * + *
+     * {index} - the current parameter index
+     * {0} - the first parameter value
+     * {1} - the second parameter value
+     * etc...
+    
* + * + * + * Default value is "{index}" for compatibility with previous JUnit versions. + * + * @return [java.text.MessageFormat] pattern string, except the index placeholder. + * @see java.text.MessageFormat + */ + val name: String = "{index}" + ) + + /** + * Annotation for fields of the test class which will be initialized by the method annotated by + * `Parameters`

+ * By using directly this annotation, the test class constructor isn't needed.

+ * Index range must start at 0. Default value is 0. + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FIELD) + annotation class Parameter( + /** + * Method that returns the index of the parameter in the array returned by the method annotated + * by `Parameters`.

+ * Index range must start at 0. Default value is 0. + * + * @return the index of the parameter. + */ + val value: Int = 0 + ) + + private class TestClassRunnerForParameters( + type: Class<*>, + private val parametersIndex: Int, + val customName: String + ) : AndroidJUnit4ClassRunner(type) { + + @Throws(Exception::class) + fun createTestInstance(bootstrappedClass: Class<*>): Any { + val constructors = bootstrappedClass.constructors + Assert.assertEquals(1, constructors.size.toLong()) + if (!fieldsAreAnnotated()) { + return constructors[0]!!.newInstance(*computeParams(bootstrappedClass.getClassLoader())) + } else { + val instance: Any = constructors[0]!!.newInstance() + injectParametersIntoFields(instance, bootstrappedClass.getClassLoader()) + return instance + } + } + + @Throws(Exception::class) + fun computeParams(classLoader: ClassLoader): Array { + // Robolectric uses a different class loader when running the tests, so the parameters objects + // created by the test runner are not compatible with the parameters required by the test. + // Instead, we compute the parameters within the test's class loader. + try { + val parametersList = getParametersList(testClass, classLoader) + + if (parametersIndex >= parametersList.size) { + throw Exception( + "Re-computing the parameter list returned a different number of " + + "parameters values. Is the data() method of your test non-deterministic?" + ) + } + val parametersObj = parametersList.get(parametersIndex) + return if (parametersObj is Array<*> && parametersObj.isArrayOf()) + parametersObj as Array + else + arrayOf(parametersObj) + } catch (e: ClassCastException) { + throw Exception( + String.format( + "%s.%s() must return a Collection of arrays.", testClass.getName(), customName + ) + ) + } catch (exception: Exception) { + throw exception + } catch (throwable: Throwable) { + throw Exception(throwable) + } + } + + @Throws(Exception::class) + fun injectParametersIntoFields(testClassInstance: Any, classLoader: ClassLoader) { + // Robolectric uses a different class loader when running the tests, so referencing Parameter + // directly causes type mismatches. Instead, we find its class within the test's class loader. + val parameterClass = getClassInClassLoader(Parameter::class.java, classLoader) + val parameters = computeParams(classLoader) + val parameterFieldsFound = HashSet() + for (field in testClassInstance.javaClass.getFields()) { + val parameter = field.getAnnotation(parameterClass as Class?) + if (parameter != null) { + val index = callInstanceMethod(parameter, "value") ?: continue + parameterFieldsFound.add(index) + try { + field.set(testClassInstance, parameters[index]) + } catch (iare: IllegalArgumentException) { + throw Exception( + (testClass.getName() + + ": Trying to set " + + field.getName() + + " with the value " + + parameters[index] + + " that is not the right type (" + + parameters[index]!!.javaClass.getSimpleName() + + " instead of " + + field.type.getSimpleName() + + ")."), + iare + ) + } + } + } + check(parameterFieldsFound.size == parameters.size) { + String.Companion.format( + Locale.US, + "Provided %d parameters, but only found fields for parameters: %s", + parameters.size, + parameterFieldsFound.toString() + ) + } + } + + override fun testName(method: FrameworkMethod): String { + return method.name + this.customName + } + + override fun validateConstructor(errors: MutableList?) { + validateOnlyOneConstructor(errors) + if (fieldsAreAnnotated()) { + validateZeroArgConstructor(errors) + } + } + + override fun toString(): String { + return "TestClassRunnerForParameters $customName" + } + + override fun validateFields(errors: MutableList) { + super.validateFields(errors) + // Ensure that indexes for parameters are correctly defined + if (fieldsAreAnnotated()) { + val annotatedFieldsByParameter: MutableList = + this.annotatedFieldsByParameter + val usedIndices = IntArray(annotatedFieldsByParameter.size) + for (each in annotatedFieldsByParameter) { + val index: Int = each.field + .getAnnotation( + Parameter::class.java + ).value + if (index < 0 || index > annotatedFieldsByParameter.size - 1) { + errors.add( + Exception( + ("Invalid @Parameter value: " + + index + + ". @Parameter fields counted: " + + annotatedFieldsByParameter.size + + ". Please use an index between 0 and " + + (annotatedFieldsByParameter.size - 1) + + ".") + ) + ) + } else { + usedIndices[index]++ + } + } + for (index in usedIndices.indices) { + val numberOfUse = usedIndices[index] + if (numberOfUse == 0) { + errors.add(Exception("@Parameter($index) is never used.")) + } else if (numberOfUse > 1) { + errors.add(Exception("@Parameter($index) is used more than once ($numberOfUse).")) + } + } + } + } + + val annotatedFieldsByParameter: MutableList + get() = testClass.getAnnotatedFields(Parameter::class.java) + + fun fieldsAreAnnotated(): Boolean { + return !this.annotatedFieldsByParameter.isEmpty() + } + + companion object { + fun callInstanceMethod( + instance: Any, methodName: String, vararg classParameters: ClassParameter<*>? + ): R? { + PerfStatsCollector.instance.incrementCount( + String.format( + "ReflectionHelpers.callInstanceMethod-%s_%s", + instance.javaClass.getName(), methodName + ) + ) + try { + val classes: Array?> = ClassParameter.getClasses(*classParameters) + val values: Array = ClassParameter.getValues(*classParameters) + + return traverseClassHierarchy( + instance.javaClass, + NoSuchMethodException::class.java, + { traversalClass: Class<*>? -> + val declaredMethod = traversalClass!!.getDeclaredMethod(methodName, *classes) + declaredMethod.isAccessible = true + declaredMethod.invoke(instance, *values) as R? + }) + } catch (e: InvocationTargetException) { + if (e.targetException is java.lang.RuntimeException) { + throw e.targetException as java.lang.RuntimeException? as Throwable + } + if (e.targetException is Error) { + throw e.targetException as Error? as Throwable + } + throw java.lang.RuntimeException(e.targetException) + } catch (e: java.lang.Exception) { + throw java.lang.RuntimeException(e) + } + } + + @Throws(java.lang.Exception::class) + private fun traverseClassHierarchy( + targetClass: Class<*>, exceptionClass: Class, insideTraversal: InsideTraversal + ): R? { + var hierarchyTraversalClass = targetClass + while (true) { + try { + return insideTraversal.run(hierarchyTraversalClass) + } catch (e: java.lang.Exception) { + if (!exceptionClass.isInstance(e)) { + throw e + } + hierarchyTraversalClass = hierarchyTraversalClass.getSuperclass() + if (hierarchyTraversalClass == null) { + throw java.lang.RuntimeException(e) + } + } + } + } + } + } + + private val runners = ArrayList() + + /* + * Only called reflectively. Do not use programmatically. + */ + init { + val testClass = getTestClass() + val classLoader = javaClass.getClassLoader() + if (classLoader != null) { + val parameters: Parameters = + getParametersMethod( + testClass, + classLoader + ).getAnnotation(Parameters::class.java) + val parametersList = getParametersList(testClass, classLoader) + for (i in parametersList.indices) { + val parametersObj = parametersList[i] + val parameterArray = if (parametersObj is Array<*> && parametersObj.isArrayOf()) { + parametersObj + } else { + arrayOf(parametersObj) + } + runners.add( + TestClassRunnerForParameters( + testClass.javaClass, i, nameFor(parameters.name, i, parameterArray) + ) + ) + } + } + + } + + override fun getChildren(): MutableList { + return runners + } + + companion object { + @Throws(Throwable::class) + private fun getParametersList( + testClass: TestClass, + classLoader: ClassLoader + ): MutableList { + val parameters = getParametersMethod(testClass, classLoader).invokeExplosively(null) + if (parameters != null && parameters.javaClass.isArray) { + return Arrays.asList(*parameters as Array) + } else { + return parameters as MutableList + } + } + + @Throws(Exception::class) + private fun getParametersMethod( + testClass: TestClass, + classLoader: ClassLoader + ): FrameworkMethod { + val methods = testClass.getAnnotatedMethods(Parameters::class.java) + for (each in methods) { + val modifiers = each.method.modifiers + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { + return getFrameworkMethodInClassLoader(each, classLoader) + } + } + + throw Exception("No public static parameters method on class " + testClass.getName()) + } + + private fun nameFor(namePattern: String, index: Int, parameters: Array): String { + val finalPattern = namePattern.replace("\\{index\\}".toRegex(), index.toString()) + val name = MessageFormat.format(finalPattern, *parameters) + return "[" + name + "]" + } + + /** + * Returns the [FrameworkMethod] object for the given method in the provided class loader. + */ + @Throws(ClassNotFoundException::class, NoSuchMethodException::class) + private fun getFrameworkMethodInClassLoader( + method: FrameworkMethod, classLoader: ClassLoader + ): FrameworkMethod { + val methodInClassLoader = getMethodInClassLoader(method.method, classLoader) + if (methodInClassLoader == method.method) { + // The method was already loaded in the right class loader, return it as is. + return method + } + return FrameworkMethod(methodInClassLoader) + } + + /** Returns the [java.lang.reflect.Method] object for the given method in the provided class loader. */ + @Throws(ClassNotFoundException::class, NoSuchMethodException::class) + private fun getMethodInClassLoader(method: Method, classLoader: ClassLoader): Method { + val declaringClass = method.declaringClass + + if (declaringClass.getClassLoader() === classLoader) { + // The method was already loaded in the right class loader, return it as is. + return method + } + + // Find the class in the class loader corresponding to the declaring class of the method. + val declaringClassInClassLoader = getClassInClassLoader(declaringClass, classLoader) + + // Find the method with the same signature in the class loader. + return declaringClassInClassLoader.getMethod(method.name, *method.getParameterTypes()) + } + + /** Returns the [Class] object for the given class in the provided class loader. */ + @Throws(ClassNotFoundException::class) + private fun getClassInClassLoader(klass: Class<*>, classLoader: ClassLoader): Class<*> { + if (klass.getClassLoader() === classLoader) { + // The method was already loaded in the right class loader, return it as is. + return klass + } + + // Find the class in the class loader corresponding to the declaring class of the method. + return classLoader.loadClass(klass.getName()) + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt new file mode 100644 index 0000000000..a4b1d7f693 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt @@ -0,0 +1,13 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ +package com.flowcrypt.email.junit.suites + +/** + * @author Denys Bondarenko + */ +fun interface InsideTraversal { + @Throws(Exception::class) + fun run(traversalClass: Class<*>?): R? +} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt new file mode 100644 index 0000000000..ead8718a3a --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt @@ -0,0 +1,17 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.suites + +/** + * @author Denys Bondarenko + */ +class Metadata(metadata: MutableMap?, Any?>) { + private val metadata: MutableMap?, Any?> = java.util.HashMap?, Any?>(metadata) + + fun get(metadataClass: Class): T? { + return metadataClass.cast(metadata.get(metadataClass)) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt new file mode 100644 index 0000000000..0a5cbcf4a5 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt @@ -0,0 +1,82 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.suites + +/** + * @author Denys Bondarenko + */ +class Metric(val name: String?, var count: Int, elapsedNs: Int, val isSuccess: Boolean) { + var elapsedNs: Long + private set + var minNs: Long = 0 + private set + var maxNs: Long = 0 + private set + + init { + this.elapsedNs = elapsedNs.toLong() + } + + constructor(name: String?, success: Boolean) : this(name, 0, 0, success) + + fun record(elapsedNs: Long) { + if (count == 0 || elapsedNs < minNs) { + minNs = elapsedNs + } + + if (elapsedNs > maxNs) { + maxNs = elapsedNs + } + + this.elapsedNs += elapsedNs + + count++ + } + + fun incrementCount() { + this.count++ + } + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o !is Metric) { + return false + } + + val metric = o + + if (this.isSuccess != metric.isSuccess) { + return false + } + return if (name != null) (name == metric.name) else metric.name == null + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + (if (this.isSuccess) 1 else 0) + return result + } + + override fun toString(): String { + return ("Metric{" + + "name='" + + name + + '\'' + + ", count=" + + count + + ", minNs=" + + minNs + + ", maxNs=" + + maxNs + + ", elapsedNs=" + + elapsedNs + + ", success=" + + this.isSuccess + + '}') + } +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt new file mode 100644 index 0000000000..78000af94f --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt @@ -0,0 +1,154 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.junit.suites + +/** + * @author Denys Bondarenko + */ +class PerfStatsCollector internal constructor(private val clock: Clock) { + private val metadata: MutableMap?, Any?> = HashMap?, Any?>() + private val metricMap: MutableMap = + HashMap() + private var enabled = true + + constructor() : this({ System.nanoTime() }) + + /** If not enabled, don't bother retaining perf stats, saving some memory and CPU cycles. */ + fun setEnabled(isEnabled: Boolean) { + this.enabled = isEnabled + } + + fun startEvent(eventName: String?): Event { + return Event(eventName) + } + + fun measure( + eventName: String?, + supplier: ThrowingSupplier + ): T? { + var success = true + val event = startEvent(eventName) + try { + return supplier.get() + } catch (e: java.lang.Exception) { + success = false + throw e + } finally { + event.finished(success) + } + } + + fun incrementCount(eventName: String?) { + synchronized(this@PerfStatsCollector) { + val key = MetricKey(eventName, true) + var metric = metricMap[key] + if (metric == null) { + metricMap.put( + key, + Metric(key.name, key.success).also { metric = it }) + } + metric?.incrementCount() + } + } + + /** Supplier that throws an exception. */ // @FunctionalInterface -- not available on Android yet... + interface ThrowingSupplier { + fun get(): T? + } + + fun measure(eventName: String?, runnable: ThrowingRunnable) { + var success = true + val event = startEvent(eventName) + try { + runnable.run() + } catch (e: java.lang.Exception) { + success = false + throw e + } finally { + event.finished(success) + } + } + + /** Runnable that throws an exception. */ // @FunctionalInterface -- not available on Android yet... + interface ThrowingRunnable { + fun run() + } + + @get:Synchronized + val metrics: MutableCollection + get() = java.util.ArrayList(metricMap.values) + + @Synchronized + fun putMetadata(metadataClass: Class?, metadata: T?) { + if (!enabled) { + return + } + + this.metadata.put(metadataClass, metadata) + } + + @Synchronized + fun getMetadata(): Metadata { + return Metadata(metadata) + } + + fun reset() { + metadata.clear() + metricMap.clear() + } + + /** Event for perf stats collection. */ + inner class Event internal constructor(private val name: String?) { + private val startTimeNs: Long = clock.nanoTime() + + @JvmOverloads + fun finished(success: Boolean = true) { + if (!enabled) { + return + } + + synchronized(this@PerfStatsCollector) { + val key = MetricKey(name, success) + var metric = metricMap[key] + if (metric == null) { + metricMap.put( + key, + Metric(key.name, key.success).also { metric = it }) + } + metric?.record(clock.nanoTime() - startTimeNs) + } + } + } + + /** Metric key for perf stats collection. */ + private class MetricKey(val name: String?, val success: Boolean) { + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o !is MetricKey) { + return false + } + + val metricKey = o + + if (success != metricKey.success) { + return false + } + return if (name != null) (name == metricKey.name) else metricKey.name == null + } + + override fun hashCode(): Int { + var result = if (name != null) name.hashCode() else 0 + result = 31 * result + (if (success) 1 else 0) + return result + } + } + + companion object { + val instance: PerfStatsCollector = PerfStatsCollector() + } +} \ No newline at end of file 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 3d5f235e5d..25e4896451 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt @@ -5,22 +5,10 @@ package com.flowcrypt.email.ui -import androidx.test.espresso.Espresso.onView -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.scrollTo -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.RootMatchers.isDialog -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.activityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.flowcrypt.email.R -import com.flowcrypt.email.TestConstants +import com.flowcrypt.email.junit.suites.CustomSuite import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule @@ -29,9 +17,6 @@ import com.flowcrypt.email.ui.activity.MainActivity import com.flowcrypt.email.ui.base.AddOtherAccountBaseTest import com.flowcrypt.email.util.AuthCredentialsManager import com.flowcrypt.email.util.TestGeneralUtil -import org.hamcrest.CoreMatchers.anyOf -import org.hamcrest.CoreMatchers.startsWith -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -42,7 +27,7 @@ import org.junit.runner.RunWith * @author Denys Bondarenko */ @MediumTest -@RunWith(AndroidJUnit4::class) +@RunWith(CustomSuite::class) class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { override val activeActivityRule = activityScenarioRule( @@ -60,82 +45,27 @@ class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { .around(ScreenshotTestRule()) @Test - @Ignore("flaky 8") - fun testShowWarningIfAuthFail() { + fun one() { enableAdvancedMode() val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") fillAllFields(credentials) - val someFailTextToChangeRightValue = "123" + } - val fieldIdentifiersWithIncorrectData = intArrayOf( - R.id.editTextEmail, - R.id.editTextUserName, - R.id.editTextImapServer, - R.id.editTextImapPort, - R.id.editTextSmtpServer, - R.id.editTextSmtpPort, - R.id.editTextSmtpUsername, - R.id.editTextSmtpPassword - ) + @Test + fun two() { + enableAdvancedMode() + val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") + fillAllFields(credentials) + } - val correctData = arrayOf( - credentials.email, - credentials.username, - credentials.password, - credentials.imapServer, - credentials.imapPort.toString(), - credentials.smtpServer, - credentials.smtpPort.toString(), - credentials.smtpSigInUsername, - credentials.smtpSignInPassword + companion object { + @JvmStatic + // name argument is optional, it will show up on the test results + @CustomSuite.Parameters(name = "Input: {0}") + // parameters are provided as arrays, allowing more than one parameter + fun params() = listOf( + arrayOf(1), + arrayOf(2), ) - - val numberOfChecks = if (credentials.hasCustomSignInForSmtp) { - fieldIdentifiersWithIncorrectData.size - } else { - fieldIdentifiersWithIncorrectData.size - 2 - } - - for (i in 0 until numberOfChecks) { - onView(withId(fieldIdentifiersWithIncorrectData[i])) - .perform( - scrollTo(), - typeText(someFailTextToChangeRightValue), - closeSoftKeyboard() - ) - onView(withId(R.id.buttonTryToConnect)) - .perform(scrollTo(), click()) - - onView( - anyOf( - withText(startsWith(TestConstants.IMAP)), - withText(startsWith(TestConstants.SMTP)) - ) - ).check(matches(isDisplayed())) - - if (i in intArrayOf( - R.id.editTextImapServer, - R.id.editTextImapPort, - R.id.editTextSmtpServer, - R.id.editTextSmtpPort - ) - ) { - onView(withText(getResString(R.string.network_error))) - .check(matches(isDisplayed())) - } - - onView(withText(getResString(R.string.cancel))) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - .perform(click()) - - onView(withId(fieldIdentifiersWithIncorrectData[i])) - .perform( - scrollTo(), - clearText(), - typeText(correctData[i]), - closeSoftKeyboard() - ) - } } } From 59dd9ff6a6497c46fef5c29848d9ef7eef39aedb Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Mar 2025 16:20:32 +0200 Subject: [PATCH 06/25] Refactored code --- .../RepeatableAndroidJUnit4ClassRunner.kt | 62 +++ .../email/junit/suites/ClassParameter.kt | 51 --- .../com/flowcrypt/email/junit/suites/Clock.kt | 13 - .../email/junit/suites/CustomSuite.kt | 402 ------------------ .../email/junit/suites/InsideTraversal.kt | 13 - .../flowcrypt/email/junit/suites/Metadata.kt | 17 - .../flowcrypt/email/junit/suites/Metric.kt | 82 ---- .../email/junit/suites/PerfStatsCollector.kt | 154 ------- .../email/ui/AddOtherAccountFlowTest.kt | 18 +- 9 files changed, 67 insertions(+), 745 deletions(-) create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/runner/RepeatableAndroidJUnit4ClassRunner.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt 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/junit/suites/ClassParameter.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt deleted file mode 100644 index dbdb549306..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/ClassParameter.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.junit.suites - -/** - * @author Denys Bondarenko - */ -/** - * Typed parameter used with reflective method calls. - * - * @param The value of the method parameter. - */ -class ClassParameter(val clazz: Class?, val value: V?) { - companion object { - fun from(clazz: Class?, value: V?): ClassParameter { - return ClassParameter(clazz, value) - } - - fun fromComponentLists( - classes: Array?>, - values: Array - ): Array?> { - val classParameters = arrayOfNulls>(classes.size) - for (i in classes.indices) { - classParameters[i] = from(classes[i], values[i]) - } - return classParameters - } - - fun getClasses(vararg classParameters: ClassParameter<*>?): Array?> { - val classes = arrayOfNulls>(classParameters.size) - for (i in classParameters.indices) { - val paramClass: Class<*>? = classParameters[i]!!.clazz - classes[i] = paramClass - } - return classes - } - - fun getValues(vararg classParameters: ClassParameter<*>?): Array { - val values = arrayOfNulls(classParameters.size) - for (i in classParameters.indices) { - val paramValue: Any? = classParameters[i]!!.value - values[i] = paramValue - } - return values - } - } -} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt deleted file mode 100644 index 399da337fb..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Clock.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.junit.suites - -/** - * @author Denys Bondarenko - */ -internal fun interface Clock { - fun nanoTime(): Long -} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt deleted file mode 100644 index c551a5eac7..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/CustomSuite.kt +++ /dev/null @@ -1,402 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.junit.suites - -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner -import org.junit.Assert -import org.junit.runner.Runner -import org.junit.runners.Suite -import org.junit.runners.model.FrameworkField -import org.junit.runners.model.FrameworkMethod -import org.junit.runners.model.TestClass -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.text.MessageFormat -import java.util.Arrays -import java.util.Locale - -/** - * @author Denys Bondarenko - */ -class CustomSuite(klass: Class<*>?) : Suite(klass, mutableListOf()) { - /** - * Annotation for a method which provides parameters to be injected into the test class - * constructor by `Parameterized` - */ - @Retention(AnnotationRetention.RUNTIME) - @Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.PROPERTY_SETTER - ) - annotation class Parameters( - /** - * Optional pattern to derive the test's name from the parameters. Use numbers in braces to - * refer to the parameters or the additional data as follows: - * - *
-     * {index} - the current parameter index
-     * {0} - the first parameter value
-     * {1} - the second parameter value
-     * etc...
-    
* - * - * - * Default value is "{index}" for compatibility with previous JUnit versions. - * - * @return [java.text.MessageFormat] pattern string, except the index placeholder. - * @see java.text.MessageFormat - */ - val name: String = "{index}" - ) - - /** - * Annotation for fields of the test class which will be initialized by the method annotated by - * `Parameters`

- * By using directly this annotation, the test class constructor isn't needed.

- * Index range must start at 0. Default value is 0. - */ - @Retention(AnnotationRetention.RUNTIME) - @Target(AnnotationTarget.FIELD) - annotation class Parameter( - /** - * Method that returns the index of the parameter in the array returned by the method annotated - * by `Parameters`.

- * Index range must start at 0. Default value is 0. - * - * @return the index of the parameter. - */ - val value: Int = 0 - ) - - private class TestClassRunnerForParameters( - type: Class<*>, - private val parametersIndex: Int, - val customName: String - ) : AndroidJUnit4ClassRunner(type) { - - @Throws(Exception::class) - fun createTestInstance(bootstrappedClass: Class<*>): Any { - val constructors = bootstrappedClass.constructors - Assert.assertEquals(1, constructors.size.toLong()) - if (!fieldsAreAnnotated()) { - return constructors[0]!!.newInstance(*computeParams(bootstrappedClass.getClassLoader())) - } else { - val instance: Any = constructors[0]!!.newInstance() - injectParametersIntoFields(instance, bootstrappedClass.getClassLoader()) - return instance - } - } - - @Throws(Exception::class) - fun computeParams(classLoader: ClassLoader): Array { - // Robolectric uses a different class loader when running the tests, so the parameters objects - // created by the test runner are not compatible with the parameters required by the test. - // Instead, we compute the parameters within the test's class loader. - try { - val parametersList = getParametersList(testClass, classLoader) - - if (parametersIndex >= parametersList.size) { - throw Exception( - "Re-computing the parameter list returned a different number of " - + "parameters values. Is the data() method of your test non-deterministic?" - ) - } - val parametersObj = parametersList.get(parametersIndex) - return if (parametersObj is Array<*> && parametersObj.isArrayOf()) - parametersObj as Array - else - arrayOf(parametersObj) - } catch (e: ClassCastException) { - throw Exception( - String.format( - "%s.%s() must return a Collection of arrays.", testClass.getName(), customName - ) - ) - } catch (exception: Exception) { - throw exception - } catch (throwable: Throwable) { - throw Exception(throwable) - } - } - - @Throws(Exception::class) - fun injectParametersIntoFields(testClassInstance: Any, classLoader: ClassLoader) { - // Robolectric uses a different class loader when running the tests, so referencing Parameter - // directly causes type mismatches. Instead, we find its class within the test's class loader. - val parameterClass = getClassInClassLoader(Parameter::class.java, classLoader) - val parameters = computeParams(classLoader) - val parameterFieldsFound = HashSet() - for (field in testClassInstance.javaClass.getFields()) { - val parameter = field.getAnnotation(parameterClass as Class?) - if (parameter != null) { - val index = callInstanceMethod(parameter, "value") ?: continue - parameterFieldsFound.add(index) - try { - field.set(testClassInstance, parameters[index]) - } catch (iare: IllegalArgumentException) { - throw Exception( - (testClass.getName() - + ": Trying to set " - + field.getName() - + " with the value " - + parameters[index] - + " that is not the right type (" - + parameters[index]!!.javaClass.getSimpleName() - + " instead of " - + field.type.getSimpleName() - + ")."), - iare - ) - } - } - } - check(parameterFieldsFound.size == parameters.size) { - String.Companion.format( - Locale.US, - "Provided %d parameters, but only found fields for parameters: %s", - parameters.size, - parameterFieldsFound.toString() - ) - } - } - - override fun testName(method: FrameworkMethod): String { - return method.name + this.customName - } - - override fun validateConstructor(errors: MutableList?) { - validateOnlyOneConstructor(errors) - if (fieldsAreAnnotated()) { - validateZeroArgConstructor(errors) - } - } - - override fun toString(): String { - return "TestClassRunnerForParameters $customName" - } - - override fun validateFields(errors: MutableList) { - super.validateFields(errors) - // Ensure that indexes for parameters are correctly defined - if (fieldsAreAnnotated()) { - val annotatedFieldsByParameter: MutableList = - this.annotatedFieldsByParameter - val usedIndices = IntArray(annotatedFieldsByParameter.size) - for (each in annotatedFieldsByParameter) { - val index: Int = each.field - .getAnnotation( - Parameter::class.java - ).value - if (index < 0 || index > annotatedFieldsByParameter.size - 1) { - errors.add( - Exception( - ("Invalid @Parameter value: " - + index - + ". @Parameter fields counted: " - + annotatedFieldsByParameter.size - + ". Please use an index between 0 and " - + (annotatedFieldsByParameter.size - 1) - + ".") - ) - ) - } else { - usedIndices[index]++ - } - } - for (index in usedIndices.indices) { - val numberOfUse = usedIndices[index] - if (numberOfUse == 0) { - errors.add(Exception("@Parameter($index) is never used.")) - } else if (numberOfUse > 1) { - errors.add(Exception("@Parameter($index) is used more than once ($numberOfUse).")) - } - } - } - } - - val annotatedFieldsByParameter: MutableList - get() = testClass.getAnnotatedFields(Parameter::class.java) - - fun fieldsAreAnnotated(): Boolean { - return !this.annotatedFieldsByParameter.isEmpty() - } - - companion object { - fun callInstanceMethod( - instance: Any, methodName: String, vararg classParameters: ClassParameter<*>? - ): R? { - PerfStatsCollector.instance.incrementCount( - String.format( - "ReflectionHelpers.callInstanceMethod-%s_%s", - instance.javaClass.getName(), methodName - ) - ) - try { - val classes: Array?> = ClassParameter.getClasses(*classParameters) - val values: Array = ClassParameter.getValues(*classParameters) - - return traverseClassHierarchy( - instance.javaClass, - NoSuchMethodException::class.java, - { traversalClass: Class<*>? -> - val declaredMethod = traversalClass!!.getDeclaredMethod(methodName, *classes) - declaredMethod.isAccessible = true - declaredMethod.invoke(instance, *values) as R? - }) - } catch (e: InvocationTargetException) { - if (e.targetException is java.lang.RuntimeException) { - throw e.targetException as java.lang.RuntimeException? as Throwable - } - if (e.targetException is Error) { - throw e.targetException as Error? as Throwable - } - throw java.lang.RuntimeException(e.targetException) - } catch (e: java.lang.Exception) { - throw java.lang.RuntimeException(e) - } - } - - @Throws(java.lang.Exception::class) - private fun traverseClassHierarchy( - targetClass: Class<*>, exceptionClass: Class, insideTraversal: InsideTraversal - ): R? { - var hierarchyTraversalClass = targetClass - while (true) { - try { - return insideTraversal.run(hierarchyTraversalClass) - } catch (e: java.lang.Exception) { - if (!exceptionClass.isInstance(e)) { - throw e - } - hierarchyTraversalClass = hierarchyTraversalClass.getSuperclass() - if (hierarchyTraversalClass == null) { - throw java.lang.RuntimeException(e) - } - } - } - } - } - } - - private val runners = ArrayList() - - /* - * Only called reflectively. Do not use programmatically. - */ - init { - val testClass = getTestClass() - val classLoader = javaClass.getClassLoader() - if (classLoader != null) { - val parameters: Parameters = - getParametersMethod( - testClass, - classLoader - ).getAnnotation(Parameters::class.java) - val parametersList = getParametersList(testClass, classLoader) - for (i in parametersList.indices) { - val parametersObj = parametersList[i] - val parameterArray = if (parametersObj is Array<*> && parametersObj.isArrayOf()) { - parametersObj - } else { - arrayOf(parametersObj) - } - runners.add( - TestClassRunnerForParameters( - testClass.javaClass, i, nameFor(parameters.name, i, parameterArray) - ) - ) - } - } - - } - - override fun getChildren(): MutableList { - return runners - } - - companion object { - @Throws(Throwable::class) - private fun getParametersList( - testClass: TestClass, - classLoader: ClassLoader - ): MutableList { - val parameters = getParametersMethod(testClass, classLoader).invokeExplosively(null) - if (parameters != null && parameters.javaClass.isArray) { - return Arrays.asList(*parameters as Array) - } else { - return parameters as MutableList - } - } - - @Throws(Exception::class) - private fun getParametersMethod( - testClass: TestClass, - classLoader: ClassLoader - ): FrameworkMethod { - val methods = testClass.getAnnotatedMethods(Parameters::class.java) - for (each in methods) { - val modifiers = each.method.modifiers - if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { - return getFrameworkMethodInClassLoader(each, classLoader) - } - } - - throw Exception("No public static parameters method on class " + testClass.getName()) - } - - private fun nameFor(namePattern: String, index: Int, parameters: Array): String { - val finalPattern = namePattern.replace("\\{index\\}".toRegex(), index.toString()) - val name = MessageFormat.format(finalPattern, *parameters) - return "[" + name + "]" - } - - /** - * Returns the [FrameworkMethod] object for the given method in the provided class loader. - */ - @Throws(ClassNotFoundException::class, NoSuchMethodException::class) - private fun getFrameworkMethodInClassLoader( - method: FrameworkMethod, classLoader: ClassLoader - ): FrameworkMethod { - val methodInClassLoader = getMethodInClassLoader(method.method, classLoader) - if (methodInClassLoader == method.method) { - // The method was already loaded in the right class loader, return it as is. - return method - } - return FrameworkMethod(methodInClassLoader) - } - - /** Returns the [java.lang.reflect.Method] object for the given method in the provided class loader. */ - @Throws(ClassNotFoundException::class, NoSuchMethodException::class) - private fun getMethodInClassLoader(method: Method, classLoader: ClassLoader): Method { - val declaringClass = method.declaringClass - - if (declaringClass.getClassLoader() === classLoader) { - // The method was already loaded in the right class loader, return it as is. - return method - } - - // Find the class in the class loader corresponding to the declaring class of the method. - val declaringClassInClassLoader = getClassInClassLoader(declaringClass, classLoader) - - // Find the method with the same signature in the class loader. - return declaringClassInClassLoader.getMethod(method.name, *method.getParameterTypes()) - } - - /** Returns the [Class] object for the given class in the provided class loader. */ - @Throws(ClassNotFoundException::class) - private fun getClassInClassLoader(klass: Class<*>, classLoader: ClassLoader): Class<*> { - if (klass.getClassLoader() === classLoader) { - // The method was already loaded in the right class loader, return it as is. - return klass - } - - // Find the class in the class loader corresponding to the declaring class of the method. - return classLoader.loadClass(klass.getName()) - } - } -} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt deleted file mode 100644 index a4b1d7f693..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/InsideTraversal.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ -package com.flowcrypt.email.junit.suites - -/** - * @author Denys Bondarenko - */ -fun interface InsideTraversal { - @Throws(Exception::class) - fun run(traversalClass: Class<*>?): R? -} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt deleted file mode 100644 index ead8718a3a..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metadata.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.junit.suites - -/** - * @author Denys Bondarenko - */ -class Metadata(metadata: MutableMap?, Any?>) { - private val metadata: MutableMap?, Any?> = java.util.HashMap?, Any?>(metadata) - - fun get(metadataClass: Class): T? { - return metadataClass.cast(metadata.get(metadataClass)) - } -} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt deleted file mode 100644 index 0a5cbcf4a5..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/Metric.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.junit.suites - -/** - * @author Denys Bondarenko - */ -class Metric(val name: String?, var count: Int, elapsedNs: Int, val isSuccess: Boolean) { - var elapsedNs: Long - private set - var minNs: Long = 0 - private set - var maxNs: Long = 0 - private set - - init { - this.elapsedNs = elapsedNs.toLong() - } - - constructor(name: String?, success: Boolean) : this(name, 0, 0, success) - - fun record(elapsedNs: Long) { - if (count == 0 || elapsedNs < minNs) { - minNs = elapsedNs - } - - if (elapsedNs > maxNs) { - maxNs = elapsedNs - } - - this.elapsedNs += elapsedNs - - count++ - } - - fun incrementCount() { - this.count++ - } - - override fun equals(o: Any?): Boolean { - if (this === o) { - return true - } - if (o !is Metric) { - return false - } - - val metric = o - - if (this.isSuccess != metric.isSuccess) { - return false - } - return if (name != null) (name == metric.name) else metric.name == null - } - - override fun hashCode(): Int { - var result = name?.hashCode() ?: 0 - result = 31 * result + (if (this.isSuccess) 1 else 0) - return result - } - - override fun toString(): String { - return ("Metric{" - + "name='" - + name - + '\'' - + ", count=" - + count - + ", minNs=" - + minNs - + ", maxNs=" - + maxNs - + ", elapsedNs=" - + elapsedNs - + ", success=" - + this.isSuccess - + '}') - } -} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt deleted file mode 100644 index 78000af94f..0000000000 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/junit/suites/PerfStatsCollector.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.junit.suites - -/** - * @author Denys Bondarenko - */ -class PerfStatsCollector internal constructor(private val clock: Clock) { - private val metadata: MutableMap?, Any?> = HashMap?, Any?>() - private val metricMap: MutableMap = - HashMap() - private var enabled = true - - constructor() : this({ System.nanoTime() }) - - /** If not enabled, don't bother retaining perf stats, saving some memory and CPU cycles. */ - fun setEnabled(isEnabled: Boolean) { - this.enabled = isEnabled - } - - fun startEvent(eventName: String?): Event { - return Event(eventName) - } - - fun measure( - eventName: String?, - supplier: ThrowingSupplier - ): T? { - var success = true - val event = startEvent(eventName) - try { - return supplier.get() - } catch (e: java.lang.Exception) { - success = false - throw e - } finally { - event.finished(success) - } - } - - fun incrementCount(eventName: String?) { - synchronized(this@PerfStatsCollector) { - val key = MetricKey(eventName, true) - var metric = metricMap[key] - if (metric == null) { - metricMap.put( - key, - Metric(key.name, key.success).also { metric = it }) - } - metric?.incrementCount() - } - } - - /** Supplier that throws an exception. */ // @FunctionalInterface -- not available on Android yet... - interface ThrowingSupplier { - fun get(): T? - } - - fun measure(eventName: String?, runnable: ThrowingRunnable) { - var success = true - val event = startEvent(eventName) - try { - runnable.run() - } catch (e: java.lang.Exception) { - success = false - throw e - } finally { - event.finished(success) - } - } - - /** Runnable that throws an exception. */ // @FunctionalInterface -- not available on Android yet... - interface ThrowingRunnable { - fun run() - } - - @get:Synchronized - val metrics: MutableCollection - get() = java.util.ArrayList(metricMap.values) - - @Synchronized - fun putMetadata(metadataClass: Class?, metadata: T?) { - if (!enabled) { - return - } - - this.metadata.put(metadataClass, metadata) - } - - @Synchronized - fun getMetadata(): Metadata { - return Metadata(metadata) - } - - fun reset() { - metadata.clear() - metricMap.clear() - } - - /** Event for perf stats collection. */ - inner class Event internal constructor(private val name: String?) { - private val startTimeNs: Long = clock.nanoTime() - - @JvmOverloads - fun finished(success: Boolean = true) { - if (!enabled) { - return - } - - synchronized(this@PerfStatsCollector) { - val key = MetricKey(name, success) - var metric = metricMap[key] - if (metric == null) { - metricMap.put( - key, - Metric(key.name, key.success).also { metric = it }) - } - metric?.record(clock.nanoTime() - startTimeNs) - } - } - } - - /** Metric key for perf stats collection. */ - private class MetricKey(val name: String?, val success: Boolean) { - override fun equals(o: Any?): Boolean { - if (this === o) { - return true - } - if (o !is MetricKey) { - return false - } - - val metricKey = o - - if (success != metricKey.success) { - return false - } - return if (name != null) (name == metricKey.name) else metricKey.name == null - } - - override fun hashCode(): Int { - var result = if (name != null) name.hashCode() else 0 - result = 31 * result + (if (success) 1 else 0) - return result - } - } - - companion object { - val instance: PerfStatsCollector = PerfStatsCollector() - } -} \ No newline at end of file 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 25e4896451..4c3173319b 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt @@ -8,7 +8,7 @@ package com.flowcrypt.email.ui import androidx.test.ext.junit.rules.activityScenarioRule import androidx.test.filters.MediumTest import com.flowcrypt.email.R -import com.flowcrypt.email.junit.suites.CustomSuite +import com.flowcrypt.email.junit.runner.RepeatableAndroidJUnit4ClassRunner import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule @@ -17,6 +17,7 @@ import com.flowcrypt.email.ui.activity.MainActivity import com.flowcrypt.email.ui.base.AddOtherAccountBaseTest import com.flowcrypt.email.util.AuthCredentialsManager import com.flowcrypt.email.util.TestGeneralUtil +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -27,7 +28,8 @@ import org.junit.runner.RunWith * @author Denys Bondarenko */ @MediumTest -@RunWith(CustomSuite::class) +@RunWith(RepeatableAndroidJUnit4ClassRunner::class) +@RepeatableAndroidJUnit4ClassRunner.RepeatTest(3) class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { override val activeActivityRule = activityScenarioRule( @@ -52,20 +54,10 @@ class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { } @Test + @Ignore("skipp") fun two() { enableAdvancedMode() val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") fillAllFields(credentials) } - - companion object { - @JvmStatic - // name argument is optional, it will show up on the test results - @CustomSuite.Parameters(name = "Input: {0}") - // parameters are provided as arrays, allowing more than one parameter - fun params() = listOf( - arrayOf(1), - arrayOf(2), - ) - } } From 45eda72d3d1a0cb3db21cc648044a668dddf0eba Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Mar 2025 17:51:20 +0200 Subject: [PATCH 07/25] Reverted back some changes --- .../email/ui/AddOtherAccountFlowTest.kt | 100 ++++++++++++++++-- 1 file changed, 89 insertions(+), 11 deletions(-) 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 4c3173319b..3d5f235e5d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt @@ -5,10 +5,22 @@ package com.flowcrypt.email.ui +import androidx.test.espresso.Espresso.onView +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.scrollTo +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.flowcrypt.email.R -import com.flowcrypt.email.junit.runner.RepeatableAndroidJUnit4ClassRunner +import com.flowcrypt.email.TestConstants import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule @@ -17,6 +29,8 @@ import com.flowcrypt.email.ui.activity.MainActivity import com.flowcrypt.email.ui.base.AddOtherAccountBaseTest import com.flowcrypt.email.util.AuthCredentialsManager import com.flowcrypt.email.util.TestGeneralUtil +import org.hamcrest.CoreMatchers.anyOf +import org.hamcrest.CoreMatchers.startsWith import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -28,8 +42,7 @@ import org.junit.runner.RunWith * @author Denys Bondarenko */ @MediumTest -@RunWith(RepeatableAndroidJUnit4ClassRunner::class) -@RepeatableAndroidJUnit4ClassRunner.RepeatTest(3) +@RunWith(AndroidJUnit4::class) class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { override val activeActivityRule = activityScenarioRule( @@ -47,17 +60,82 @@ class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { .around(ScreenshotTestRule()) @Test - fun one() { + @Ignore("flaky 8") + fun testShowWarningIfAuthFail() { enableAdvancedMode() val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") fillAllFields(credentials) - } + val someFailTextToChangeRightValue = "123" - @Test - @Ignore("skipp") - fun two() { - enableAdvancedMode() - val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") - fillAllFields(credentials) + val fieldIdentifiersWithIncorrectData = intArrayOf( + R.id.editTextEmail, + R.id.editTextUserName, + R.id.editTextImapServer, + R.id.editTextImapPort, + R.id.editTextSmtpServer, + R.id.editTextSmtpPort, + R.id.editTextSmtpUsername, + R.id.editTextSmtpPassword + ) + + val correctData = arrayOf( + credentials.email, + credentials.username, + credentials.password, + credentials.imapServer, + credentials.imapPort.toString(), + credentials.smtpServer, + credentials.smtpPort.toString(), + credentials.smtpSigInUsername, + credentials.smtpSignInPassword + ) + + val numberOfChecks = if (credentials.hasCustomSignInForSmtp) { + fieldIdentifiersWithIncorrectData.size + } else { + fieldIdentifiersWithIncorrectData.size - 2 + } + + for (i in 0 until numberOfChecks) { + onView(withId(fieldIdentifiersWithIncorrectData[i])) + .perform( + scrollTo(), + typeText(someFailTextToChangeRightValue), + closeSoftKeyboard() + ) + onView(withId(R.id.buttonTryToConnect)) + .perform(scrollTo(), click()) + + onView( + anyOf( + withText(startsWith(TestConstants.IMAP)), + withText(startsWith(TestConstants.SMTP)) + ) + ).check(matches(isDisplayed())) + + if (i in intArrayOf( + R.id.editTextImapServer, + R.id.editTextImapPort, + R.id.editTextSmtpServer, + R.id.editTextSmtpPort + ) + ) { + onView(withText(getResString(R.string.network_error))) + .check(matches(isDisplayed())) + } + + onView(withText(getResString(R.string.cancel))) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()) + + onView(withId(fieldIdentifiersWithIncorrectData[i])) + .perform( + scrollTo(), + clearText(), + typeText(correctData[i]), + closeSoftKeyboard() + ) + } } } From 92436cc02f431511097581793357b8c83ea84357 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 25 Mar 2025 17:52:31 +0200 Subject: [PATCH 08/25] Removed unused code --- .../flowcrypt/email/CustomAndroidJUnit4.kt | 151 ------------------ 1 file changed, 151 deletions(-) delete mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/CustomAndroidJUnit4.kt 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 e6271e5dd5..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 - */ -open class CustomAndroidJUnit4(klass: Class<*>) : Runner(), Filterable, Sortable { - 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() - } - } -} From e037f86a33ab41049a199df571591310ac6eb19a Mon Sep 17 00:00:00 2001 From: denbond7 Date: Wed, 26 Mar 2025 08:13:09 +0200 Subject: [PATCH 09/25] wip --- .../java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3d5f235e5d..9172ba221b 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddOtherAccountFlowTest.kt @@ -31,7 +31,6 @@ import com.flowcrypt.email.util.AuthCredentialsManager import com.flowcrypt.email.util.TestGeneralUtil import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.startsWith -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -60,7 +59,8 @@ class AddOtherAccountFlowTest : AddOtherAccountBaseTest() { .around(ScreenshotTestRule()) @Test - @Ignore("flaky 8") + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testShowWarningIfAuthFail() { enableAdvancedMode() val credentials = AuthCredentialsManager.getAuthCredentials("user_with_not_existed_server.json") From c1ca27433fecf8dc83ae27e46be48bede6db3f41 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Wed, 26 Mar 2025 09:24:25 +0200 Subject: [PATCH 10/25] wip --- .../flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 95e9ac4239..e0d751fba0 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenDraftGmailAPIFlowTest.kt @@ -42,7 +42,6 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -182,7 +181,8 @@ class ComposeScreenDraftGmailAPIFlowTest : BaseComposeScreenTest() { .around(ScreenshotTestRule()) @Test - @Ignore("flaky 8") + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testSavingDraftViaGmailAPI() { activeActivityRule?.launch(intent) registerAllIdlingResources() From c1027059a3521ca06bdafc5314c02a92ae8cb7bd Mon Sep 17 00:00:00 2001 From: denbond7 Date: Wed, 26 Mar 2025 12:12:46 +0200 Subject: [PATCH 11/25] wip --- .../email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2e397c115e..d008a1117d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenImportRecipientPubKeyFlowTest.kt @@ -32,7 +32,6 @@ import com.flowcrypt.email.ui.adapter.RecipientChipRecyclerViewAdapter import com.flowcrypt.email.ui.base.BaseComposeScreenTest import com.flowcrypt.email.util.TestGeneralUtil import org.hamcrest.Matchers.allOf -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -91,7 +90,8 @@ class ComposeScreenImportRecipientPubKeyFlowTest : BaseComposeScreenTest() { } @Test - @Ignore("flaky 7") + //@Ignore("flaky 7") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testImportRecipientPubKeyFromClipboard() { fillDataAndMoveToImportPublicKeyScreen() addTextToClipboard("public key", publicKey) From 8ecf1ea575d950300be9ac0a4a848f88cfcf781c Mon Sep 17 00:00:00 2001 From: denbond7 Date: Wed, 26 Mar 2025 14:20:32 +0200 Subject: [PATCH 12/25] wip --- .../ComposeScreenPasswordProtectedDisallowedTermsFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a09a11764f..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 @@ -30,7 +30,6 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import org.hamcrest.Matchers.allOf -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -71,7 +70,8 @@ class ComposeScreenPasswordProtectedDisallowedTermsFlowTest : .around(ScreenshotTestRule()) @Test - @Ignore("flaky") + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testDialogWithErrorText() { intentsRelease() From 63aae701418e45f0e66447af4c0ca71cb6613a52 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 27 Mar 2025 09:35:36 +0200 Subject: [PATCH 13/25] wip --- .../email/ui/gmailapi/ThreadsListGmailApiFlowTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 96db63bf1c..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 @@ -31,7 +31,6 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.allOf -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -85,10 +84,11 @@ class ThreadsListGmailApiFlowTest : BaseGmailApiTest( .around(ScreenshotTestRule()) @Test - @Ignore("flaky") + //@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( From 2358b7fd3fd6b9bd46a4d611a48a662b5706a909 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 27 Mar 2025 18:22:40 +0200 Subject: [PATCH 14/25] wip --- .../LegalSettingsFragmentInIsolationTest.kt | 4 +- .../email/viewaction/ViewPager2Actions.kt | 140 ++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 FlowCrypt/src/androidTest/java/com/flowcrypt/email/viewaction/ViewPager2Actions.kt 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 4632251239..97406bf5ce 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 @@ -7,7 +7,6 @@ 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 @@ -26,6 +25,7 @@ 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.Ignore @@ -85,7 +85,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/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?) + } +} + From e0cf2ab951891682edcab1ebff2c1c5d2bb54ac4 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 27 Mar 2025 18:43:40 +0200 Subject: [PATCH 15/25] wip --- .../LegalSettingsFragmentInIsolationTest.kt | 18 ------------------ 1 file changed, 18 deletions(-) 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 97406bf5ce..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 @@ -6,7 +6,6 @@ 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.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isSelected @@ -14,11 +13,9 @@ 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 @@ -28,7 +25,6 @@ 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.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -62,20 +58,6 @@ class LegalSettingsFragmentInIsolationTest : BaseTest() { } @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 - @Ignore("flaky") fun testSwipeInViewPager() { onView( allOf( From 89e92960caa5f37beb3702af92f252b98736e982 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 1 Apr 2025 11:35:30 +0300 Subject: [PATCH 16/25] wip --- .../ComposeScreenForwardWithGmailApiSignatureFlowTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a003e0f601..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 @@ -73,7 +73,7 @@ class ComposeScreenForwardWithGmailApiSignatureFlowTest : .around(ScreenshotTestRule()) @Test - @Ignore("flaky") + @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) From 0d2a45f525dfa6e39ed414391e447267f5344998 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 1 Apr 2025 17:48:23 +0300 Subject: [PATCH 17/25] wip --- .../matchers/TextInputLayoutErrorMatcher.kt | 15 +++-- ...tePrivateKeyWithPassPhraseInRamFlowTest.kt | 58 ++++++++----------- 2 files changed, 35 insertions(+), 38 deletions(-) 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..570f31e459 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,25 @@ 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() + val matches: Boolean = expectedHint == actualHint + if (!matches && mismatchDescription !is Description.NullDescription) { + mismatchDescription + ?.appendText("Actual was: $actualHint") + } + return matches } } 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 e583f42e07..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,7 +215,8 @@ class UpdatePrivateKeyWithPassPhraseInRamFlowTest : BaseTest() { } @Test - @Ignore("flaky 2 2") + //@Ignore("flaky 2 2") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testPrivateKeyPassphraseAntiBruteforceProtection() { onView(withText(R.string.pass_phrase_not_provided)) .check(matches(isDisplayed())) @@ -226,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) } } From f60f8644717d497494b6f32200d47e5029b7b182 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 1 Apr 2025 19:44:36 +0300 Subject: [PATCH 18/25] wip --- .../email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fe5c90f3ed..c4b5bc4ea8 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsChangeGmailLabelsFlowTest.kt @@ -34,7 +34,6 @@ import okhttp3.mockwebserver.RecordedRequest import okio.GzipSource import okio.buffer import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -99,7 +98,8 @@ class MessageDetailsChangeGmailLabelsFlowTest : BaseGmailLabelsFlowTest() { .around(ScreenshotTestRule()) @Test - @Ignore("flaky 1") + //@Ignore("flaky 1") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testLabelsManagement() { val allLabels = initLabelIds() lastLabelIds = (allLabels.take(4) + allLabels.takeLast(1)).toMutableList() From 4fc6c2096b8f22f1d7f3b6f8d4b098a018d95d18 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 1 Apr 2025 20:49:59 +0300 Subject: [PATCH 19/25] wip --- .../email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 223e0666a3..0c19d03935 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientsFromSourceFragmentFlowTest.kt @@ -36,7 +36,6 @@ import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.CoreMatchers.containsString import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -135,7 +134,8 @@ class ImportRecipientsFromSourceFragmentFlowTest : BaseTest() { } @Test - @Ignore("flaky 4") + //@Ignore("flaky 4") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testFetchKeyFromAttesterForExistedUserImeAction() { onView(withId(R.id.eTKeyIdOrEmail)) .perform( From 03eec03c835f3a372aecd196bc9f473a929290ae Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 1 Apr 2025 20:54:25 +0300 Subject: [PATCH 20/25] wip --- .../email/matchers/TextInputLayoutErrorMatcher.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 570f31e459..9b36c49d69 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/matchers/TextInputLayoutErrorMatcher.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/matchers/TextInputLayoutErrorMatcher.kt @@ -22,11 +22,10 @@ class TextInputLayoutErrorMatcher(private val expectedHint: String) : override fun matchesSafely(item: View?, mismatchDescription: Description?): Boolean { val actualHint = (item as? TextInputLayout)?.error?.toString() - val matches: Boolean = expectedHint == actualHint - if (!matches && mismatchDescription !is Description.NullDescription) { - mismatchDescription - ?.appendText("Actual was: $actualHint") + return (expectedHint == actualHint).apply { + if (!this && mismatchDescription !is Description.NullDescription) { + mismatchDescription?.appendText("Actual was: $actualHint") + } } - return matches } } From 29944e8baf40c4954fe51148c209ff1e6b8f48d6 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Wed, 2 Apr 2025 11:16:05 +0300 Subject: [PATCH 21/25] wip --- .../com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 b2ad72effe..bf116e187f 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt @@ -50,7 +50,6 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.not -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -210,7 +209,8 @@ class MainSignInFragmentFlowTest : BaseSignTest() { } @Test - @Ignore("flaky 8") + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testClientConfigurationCombinationNotSupportedForForbidCreatingPrivateKeyMissing() { setupAndClickSignInButton( genMockGoogleSignInAccountJson( @@ -228,7 +228,8 @@ class MainSignInFragmentFlowTest : BaseSignTest() { } @Test - @Ignore("flaky 8") + //@Ignore("flaky 8") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testErrorGetPrvKeysViaEkm() { setupAndClickSignInButton(genMockGoogleSignInAccountJson(EMAIL_GET_KEYS_VIA_EKM_ERROR)) isDialogWithTextDisplayed(decorView, EKM_ERROR) From 85df3ac2ccab178624b4b457f7215f0993c76ca4 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Wed, 2 Apr 2025 17:52:17 +0300 Subject: [PATCH 22/25] wip --- .../email/ui/ComposeScreenFlowTest.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) 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 09b75e950b..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 @@ -78,7 +79,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.BeforeClass import org.junit.ClassRule -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -98,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 @@ -107,7 +106,6 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { .around(GrantPermissionRuleChooser.grant(android.Manifest.permission.POST_NOTIFICATIONS)) .around(addAccountToDatabaseRule) .around(addPrivateKeyToDatabaseRule) - .around(temporaryFolderRule) .around(activeActivityRule) .around(ScreenshotTestRule()) @@ -164,25 +162,32 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { } @Test - @Ignore("flaky 5") + //@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())) } @@ -329,7 +334,8 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { } @Test - @Ignore("flaky 4") + //@Ignore("flaky 4") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testDeletingAtts() { activeActivityRule?.launch(intent) waitForObjectWithText( @@ -547,7 +553,7 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { TimeUnit.SECONDS.toMillis(10) ) - onView(withText(att?.name)) + onView(withText(att.name)) .check(matches(isDisplayed())) } @@ -613,7 +619,7 @@ class ComposeScreenFlowTest : BaseComposeScreenTest() { TimeUnit.SECONDS.toMillis(10) ) - onView(withText(att?.name)) + onView(withText(att.name)) .check(matches(isDisplayed())) } @@ -910,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 From ca0eaff65c03df831fbfc1761acb221effbe9d24 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 3 Apr 2025 14:15:04 +0300 Subject: [PATCH 23/25] wip --- ...MultipleKeysWithPassphraseInRamFlowTest.kt | 9 +++--- .../fragment/CreateMessageFragment.kt | 28 +++++++++++++++---- .../email/ui/adapter/FromAddressesAdapter.kt | 4 ++- 3 files changed, 31 insertions(+), 10 deletions(-) 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 aa484799c4..cda2f403ff 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt @@ -26,12 +26,12 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest -import org.junit.Ignore import org.junit.Rule 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 @@ -63,16 +63,17 @@ class ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest : .around(ScreenshotTestRule()) @Test - @Ignore("flaky") + //@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/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() } From 248ae04846b51c18fe229d67afdaa17e8fb8d44a Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 3 Apr 2025 15:57:29 +0300 Subject: [PATCH 24/25] wip --- ...NoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ef176c16e2..e865eeebc7 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt @@ -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,7 +85,8 @@ class ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest : Bas } @Test - @Ignore("flaky") + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testAddEmailToExistingSingleKeyPassphraseInDatabase() { doTestAddEmailToExistingKey { //no more additional actions From 0da6688098feee735e9f7f57fb70dde471b46f9b Mon Sep 17 00:00:00 2001 From: denbond7 Date: Fri, 4 Apr 2025 10:19:25 +0300 Subject: [PATCH 25/25] wip --- .../email/ui/MessageDetailsFlowTest.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) 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 f41cc1fb8a..7e5eff780d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt @@ -85,7 +85,6 @@ import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -130,25 +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") + //@Ignore("flaky") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testTopReplyButton() { val incomingMessageInfo = testTopReplyAction(getResString(R.string.reply)) checkQuotesFunctionality(incomingMessageInfo) } @Test - @Ignore("flaky 2 3") + //@Ignore("flaky 2 3") + //RepeatableAndroidJUnit4ClassRunner 50 attempts passed fun testReplyAllButton() { val incomingMessageInfo = testStandardMsgPlaintextInternal() onView(withId(R.id.replyAllButton)) @@ -1340,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))) } }