Added privacy settings regression tests.

Affected: UI tests.

MAILAND-805
This commit is contained in:
Denys Zelenchuk 2021-01-21 17:27:43 +01:00
parent bad4ee59f4
commit d93e14d6ee
38 changed files with 635 additions and 728 deletions

View File

@ -340,8 +340,10 @@ dependencies {
`falcon`,
`espresso-contrib`,
`espresso-intents`,
`espresso-web`,
`uiautomator`,
`android-activation`
`android-activation`,
`Proton-android-instrumented-test`
)
}

View File

@ -371,6 +371,7 @@ class MessageDetailsAdapter(
webViewParams.setMargins(0, 0, 0, 0)
webView.layoutParams = webViewParams
webView.webViewClient = pmWebViewClient
webView.tag = "messageWebView"
val webSettings = webView.settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webSettings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING

View File

@ -23,7 +23,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import ch.protonmail.android.uitests.testsHelper.intentutils.IntentHelper.sendShareFileIntent
import me.proton.core.test.android.instrumented.intentutils.IntentHelper.sendShareFileIntent
class DeviceRobot {

View File

@ -23,6 +23,10 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
import androidx.test.espresso.intent.matcher.UriMatchers.hasPath
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms
import androidx.test.espresso.web.webdriver.Locator
import ch.protonmail.android.R
import ch.protonmail.android.uitests.robots.mailbox.ApplyLabelRobotInterface
import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withFolderName
@ -38,6 +42,8 @@ import ch.protonmail.android.uitests.testsHelper.TestData.pgpSignedTextDecrypted
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions
import ch.protonmail.android.uitests.testsHelper.uiactions.click
import ch.protonmail.android.uitests.testsHelper.uiactions.type
import me.proton.core.test.android.instrumented.CoreRobot
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
@ -62,6 +68,15 @@ class MessageRobot {
return this
}
fun clickLink(linkText: String): LinkNavigationDialogRobot {
UIActions.wait.forViewWithId(R.id.messageWebViewContainer)
onWebView(withTagValue(`is`(messageWebViewTag)))
.forceJavascriptEnabled()
.withElement(DriverAtoms.findElement(Locator.LINK_TEXT, linkText))
.perform(DriverAtoms.webClick())
return LinkNavigationDialogRobot()
}
fun moveFromSpamToFolder(folderName: String): SpamRobot {
UIActions.wait.forViewWithId(R.id.folders_list_view)
UIActions.allOf.clickViewWithIdAndText(R.id.folder_name, folderName)
@ -237,7 +252,19 @@ class MessageRobot {
}
}
class LinkNavigationDialogRobot {
class Verify : CoreRobot {
fun linkIsPresentInDialogMessage(link: String) {
UIActions.check.alertDialogWithPartialTextIsDisplayed(link)
}
}
inline fun verify(block: Verify.() -> Unit) = Verify().apply(block)
}
class Verify {
fun messageContainsAttachment() {
UIActions.wait.forViewWithId(R.id.attachment_title)
}
@ -260,7 +287,6 @@ class MessageRobot {
fun pgpEncryptedMessageDecrypted() {
UIActions.wait.forViewWithTextByUiAutomator(pgpEncryptedTextDecrypted)
}
fun pgpSignedMessageDecrypted() {
@ -276,6 +302,11 @@ class MessageRobot {
UIActions.wait.untilViewWithIdIsNotShown(R.id.containerLoadEmbeddedImagesContainer)
}
fun showRemoteContentButtonIsGone() {
UIActions.wait.forViewWithId(R.id.messageWebViewContainer)
UIActions.wait.untilViewWithIdIsNotShown(R.id.containerDisplayImages)
}
fun intentWithActionFileNameAndMimeTypeSent(fileName: String, mimeType: String) {
UIActions.wait.forIntent(
allOf(
@ -292,10 +323,6 @@ class MessageRobot {
companion object {
const val sendMessageId = R.id.send_message
const val attachmentToggleId = R.id.attachments_toggle
const val attachmentTitleId = R.id.attachment_title
const val attachmentNameId = R.id.attachment_name
const val downloadAttachmentButtonId = R.id.attachment_name
const val messageWebViewTag = "messageWebView"
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings
import androidx.annotation.IdRes
import androidx.appcompat.widget.SwitchCompat
import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader
import ch.protonmail.android.uitests.robots.settings.account.privacy.PrivacySettingsRobot
import me.proton.core.test.android.instrumented.CoreRobot
object SettingsActions : CoreRobot {
fun changeToggleState(state: Boolean, tag: String, @IdRes switch: SwitchCompat) {
val currentSwitchState = switch.isChecked
when (state xor currentSwitchState) {
true -> {
clickSwitchToggleView(tag, switch.id)
}
false -> {
clickSwitchToggleView(tag, switch.id)
clickSwitchToggleView(tag, switch.id)
}
}
}
private fun clickSwitchToggleView(title: String, @IdRes switchId: Int) {
recyclerView
.withId(PrivacySettingsRobot.settingsRecyclerView)
.onHolderItem(withSettingsHeader(title))
.onItemChildView(view.withId(switchId))
.click()
}
}

View File

@ -20,10 +20,9 @@ package ch.protonmail.android.uitests.robots.settings
import ch.protonmail.android.R
import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot
import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot
import ch.protonmail.android.uitests.robots.menu.MenuRobot
import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader
import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsValue
import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot
import ch.protonmail.android.uitests.robots.settings.autolock.AutoLockRobot
import ch.protonmail.android.uitests.testsHelper.StringUtils
import ch.protonmail.android.uitests.testsHelper.User
@ -44,11 +43,6 @@ class SettingsRobot {
return this
}
fun menuDrawer(): MenuRobot {
UIActions.system.clickHamburgerOrUpButton()
return MenuRobot()
}
fun openUserAccountSettings(user: User): AccountSettingsRobot {
selectSettingsItemByValue(user.email)
return AccountSettingsRobot()

View File

@ -26,6 +26,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import ch.protonmail.android.R
import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader
import ch.protonmail.android.uitests.robots.settings.SettingsRobot
import ch.protonmail.android.uitests.robots.settings.account.labelsandfolders.LabelsAndFoldersRobot
import ch.protonmail.android.uitests.robots.settings.account.privacy.PrivacySettingsRobot
import ch.protonmail.android.uitests.robots.settings.account.swipinggestures.SwipingGesturesSettingsRobot
import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions
@ -45,6 +48,11 @@ class AccountSettingsRobot {
return PasswordManagementRobot()
}
fun privacy(): PrivacySettingsRobot {
clickOnSettingsItemWithHeader(R.string.privacy)
return PrivacySettingsRobot()
}
fun recoveryEmail(): RecoveryEmailRobot {
clickOnSettingsItemWithHeader(R.string.recovery_email)
return RecoveryEmailRobot()

View File

@ -18,13 +18,16 @@
*/
package ch.protonmail.android.uitests.robots.settings.account
import androidx.annotation.StringRes
import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView
import ch.protonmail.android.R
import ch.protonmail.android.uitests.testsHelper.ActivityProvider.currentActivity
import ch.protonmail.android.uitests.robots.settings.SettingsActions.changeToggleState
import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions
import ch.protonmail.android.views.SettingsDefaultItemView
import me.proton.core.test.android.instrumented.utils.ActivityProvider
import me.proton.core.test.android.instrumented.utils.StringUtils
/**
* Class represents Display name and Signature view.
@ -32,12 +35,12 @@ import ch.protonmail.android.views.SettingsDefaultItemView
class DisplayNameAndSignatureRobot {
fun setSignatureToggleTo(state: Boolean): DisplayNameAndSignatureRobot {
changeToggleState(state, signature)
changeToggleState(state, signatureTitle, switch(signatureTitleId))
return this
}
fun setMobileSignatureToggleTo(state: Boolean): DisplayNameAndSignatureRobot {
changeToggleState(state, mobileSignature)
changeToggleState(state, mobileSignatureTitle, switch(mobileSignatureTitleId))
return this
}
@ -52,40 +55,29 @@ class DisplayNameAndSignatureRobot {
class Verify {
fun signatureToggleCheckedStateIs(state: Boolean): DisplayNameAndSignatureRobot {
UIActions.check.viewWithIdAndAncestorTagIsChecked(switchId, signature, state)
UIActions.check.viewWithIdAndAncestorTagIsChecked(switchId, signatureTitle, state)
return DisplayNameAndSignatureRobot()
}
fun mobileSignatureToggleCheckedStateIs(state: Boolean): DisplayNameAndSignatureRobot {
UIActions.check.viewWithIdAndAncestorTagIsChecked(switchId, mobileSignature, state)
UIActions.check.viewWithIdAndAncestorTagIsChecked(switchId, mobileSignatureTitle, state)
return DisplayNameAndSignatureRobot()
}
}
inline fun verify(block: Verify.() -> Unit) = Verify().apply(block)
private fun changeToggleState(state: Boolean, tag: String) {
val currentSwitchState = currentActivity!!
.findViewById<RecyclerView>(R.id.settingsRecyclerView)
.findViewWithTag<SettingsDefaultItemView>(tag)
.findViewById<SwitchCompat>(switchId)
.isChecked
when (state xor currentSwitchState) {
true -> {
UIActions.allOf.clickViewWithIdAndAncestorTag(switchId, tag)
}
false -> {
UIActions.allOf.clickViewWithIdAndAncestorTag(switchId, tag)
UIActions.allOf.clickViewWithIdAndAncestorTag(switchId, tag)
}
}
}
private fun switch(@StringRes tagId: Int) = ActivityProvider.currentActivity!!
.findViewById<RecyclerView>(R.id.settingsRecyclerView)
.findViewWithTag<SettingsDefaultItemView>(StringUtils.stringFromResource(tagId))
.findViewById<SwitchCompat>(switchId)
companion object {
private const val switchId = R.id.actionSwitch
private val signature = stringFromResource(R.string.signature)
private val mobileSignature = stringFromResource(R.string.mobile_signature)
private const val signatureTitleId = R.string.signature
private const val mobileSignatureTitleId = R.string.mobile_signature
private val signatureTitle = stringFromResource(R.string.signature)
private val mobileSignatureTitle = stringFromResource(R.string.mobile_signature)
private val displayName = stringFromResource(R.string.display_name)
}
}

View File

@ -1,22 +1,22 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
*
* This file is part of ProtonMail.
*
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account
package ch.protonmail.android.uitests.robots.settings.account.labelsandfolders
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder

View File

@ -1,22 +1,22 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
*
* This file is part of ProtonMail.
*
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account
package ch.protonmail.android.uitests.robots.settings.account.labelsandfolders
import ch.protonmail.android.R
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions

View File

@ -1,22 +1,22 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
*
* This file is part of ProtonMail.
*
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account
package ch.protonmail.android.uitests.robots.settings.account.labelsandfolders
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account.privacy
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.SwitchCompat
import ch.protonmail.android.R
import me.proton.core.test.android.instrumented.CoreRobot
import me.proton.core.test.android.instrumented.utils.ActivityProvider
class AutoDownloadMessagesRobot : CoreRobot {
fun navigateUpToPrivacySettings(): PrivacySettingsRobot {
view
.instanceOf(AppCompatImageButton::class.java)
.withParent(view.withId(R.id.toolbar))
.click()
return PrivacySettingsRobot()
}
fun enableAutoDownloadMessages(): AutoDownloadMessagesRobot {
view.withId(switchId).wait()
val switch = ActivityProvider.currentActivity!!.findViewById<SwitchCompat>(switchId)
toggleSwitch(true, switch)
return this
}
fun disableAutoDownloadMessages(): AutoDownloadMessagesRobot {
view.withId(switchId).wait()
val switch = ActivityProvider.currentActivity!!.findViewById<SwitchCompat>(switchId)
toggleSwitch(false, switch)
return this
}
private fun toggleSwitch(state: Boolean, switch: SwitchCompat) {
if (state xor switch.isChecked) {
view.withId(switch.id).click()
} else {
view.withId(switch.id).click().click()
}
}
companion object {
private const val switchId = R.id.enableFeatureSwitch
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account.privacy
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.SwitchCompat
import ch.protonmail.android.R
import me.proton.core.test.android.instrumented.CoreRobot
import me.proton.core.test.android.instrumented.utils.ActivityProvider
class BackgroundSyncRobot : CoreRobot {
fun navigateUpToPrivacySettings(): PrivacySettingsRobot {
view.instanceOf(AppCompatImageButton::class.java)
.withParent(view.withId(R.id.toolbar))
.click()
return PrivacySettingsRobot()
}
fun enableBackgroundSync(): BackgroundSyncRobot {
view.withId(switchId).wait()
val switch = ActivityProvider.currentActivity!!.findViewById<SwitchCompat>(switchId)
toggleSwitch(true, switch)
return this
}
fun disableBackgroundSync(): BackgroundSyncRobot {
view.withId(switchId).wait()
val switch = ActivityProvider.currentActivity!!.findViewById<SwitchCompat>(switchId)
toggleSwitch(false, switch)
return this
}
private fun toggleSwitch(state: Boolean, switch: SwitchCompat) {
when (state xor switch.isChecked) {
true -> {
view.withId(switch.id).click()
}
false -> {
view.withId(switch.id).click()
view.withId(switch.id).click()
}
}
}
private companion object {
private const val switchId = R.id.enableFeatureSwitch
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account.privacy
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.webClick
import androidx.test.espresso.web.webdriver.Locator
import ch.protonmail.android.R
import ch.protonmail.android.uitests.robots.settings.SettingsActions
import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader
import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions
import ch.protonmail.android.views.SettingsDefaultItemView
import me.proton.core.test.android.instrumented.CoreRobot
import me.proton.core.test.android.instrumented.utils.ActivityProvider
import me.proton.core.test.android.instrumented.utils.StringUtils.stringFromResource
class PrivacySettingsRobot : CoreRobot {
fun navigateUpToAccountSettings(): AccountSettingsRobot {
view.instanceOf(AppCompatImageButton::class.java)
.withParent(view.withId(R.id.toolbar))
.click()
return AccountSettingsRobot()
}
fun autoDownloadMessages(): AutoDownloadMessagesRobot {
selectSettingsItem(R.string.auto_download_messages_title)
return AutoDownloadMessagesRobot()
}
fun backgroundSync(): BackgroundSyncRobot {
selectSettingsItem(R.string.settings_background_sync)
return BackgroundSyncRobot()
}
fun enableAutoShowRemoteImages(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.settings_auto_show_images, true)
return this
}
fun disableAutoShowRemoteImages(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.settings_auto_show_images, false)
return this
}
fun enableAutoShowEmbeddedImages(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.settings_auto_show_embedded_images, true)
return this
}
fun disableAutoShowEmbeddedImages(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.settings_auto_show_embedded_images, false)
return this
}
fun enablePreventTakingScreenshots(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.settings_prevent_taking_screenshots, true)
return this
}
fun disablePreventTakingScreenshots(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.settings_prevent_taking_screenshots, false)
return this
}
fun enableRequestLinkConfirmation(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.hyperlink_confirmation, true)
return this
}
fun disableRequestLinkConfirmation(): PrivacySettingsRobot {
toggleSwitchWithTitle(R.string.hyperlink_confirmation, false)
return this
}
private fun selectSettingsItem(@StringRes title: Int) {
recyclerView
.withId(settingsRecyclerView)
.onHolderItem(withSettingsHeader(title))
.click()
}
private fun toggleSwitchWithTitle(@IdRes titleId: Int, value: Boolean) {
SettingsActions.changeToggleState(value, stringFromResource(titleId), switch(titleId))
}
private fun switch(@StringRes tagId: Int) = ActivityProvider.currentActivity!!
.findViewById<RecyclerView>(R.id.settingsRecyclerView)
.findViewWithTag<SettingsDefaultItemView>(stringFromResource(tagId))
.findViewById<SwitchCompat>(switchId)
/**
* Contains all the validations that can be performed by [PrivacySettingsRobot].
*/
class Verify : CoreRobot {
fun autoDownloadImagesIsEnabled() {
view.withId(R.id.valueText)
.withText(R.string.enabled)
.isDescendantOf(view.withTag(R.string.auto_download_messages_title))
.checkDisplayed()
}
fun backgroundSyncIsEnabled() {
view.withId(R.id.valueText)
.withText(R.string.disabled)
.isDescendantOf(view.withTag(R.string.settings_background_sync))
.checkDisplayed()
}
fun takingScreenshotIsDisabled() {
UIActions.check
.viewWithIdAndAncestorTagIsChecked(switchId, preventTakingScreenshotsText, false)
}
}
inline fun verify(block: Verify.() -> Unit) = Verify().apply(block)
companion object {
private const val switchId = R.id.actionSwitch
const val settingsRecyclerView = R.id.settingsRecyclerView
private val preventTakingScreenshotsText = stringFromResource(R.string.settings_prevent_taking_screenshots)
}
}

View File

@ -17,7 +17,7 @@
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account
package ch.protonmail.android.uitests.robots.settings.account.swipinggestures
import ch.protonmail.android.R
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions

View File

@ -17,10 +17,11 @@
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.robots.settings.account
package ch.protonmail.android.uitests.robots.settings.account.swipinggestures
import ch.protonmail.android.R
import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader
import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions
class SwipingGesturesSettingsRobot {

View File

@ -22,9 +22,9 @@ package ch.protonmail.android.uitests.robots.settings.autolock
import androidx.appcompat.widget.AppCompatImageButton
import ch.protonmail.android.R
import ch.protonmail.android.uitests.robots.settings.SettingsRobot
import ch.protonmail.android.uitests.tests.BaseTest.Companion.targetContext
import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions
import ch.protonmail.android.uitests.testsHelper.uiactions.click
import me.proton.core.test.android.instrumented.CoreTest.Companion.targetContext
class AutoLockRobot {

View File

@ -21,44 +21,32 @@ package ch.protonmail.android.uitests.tests
import android.Manifest.permission.READ_CONTACTS
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Activity
import android.app.Instrumentation
import android.content.Context
import android.preference.PreferenceManager
import android.util.Log
import android.widget.Toast
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.isInternal
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import ch.protonmail.android.BuildConfig
import ch.protonmail.android.activities.guest.LoginActivity
import ch.protonmail.android.uitests.testsHelper.ProtonFailureHandler
import ch.protonmail.android.uitests.testsHelper.TestData
import ch.protonmail.android.uitests.testsHelper.TestExecutionWatcher
import ch.protonmail.android.uitests.testsHelper.User
import ch.protonmail.android.uitests.testsHelper.devicesetup.DeviceSetup.clearLogcat
import ch.protonmail.android.uitests.testsHelper.devicesetup.DeviceSetup.copyAssetFileToInternalFilesStorage
import ch.protonmail.android.uitests.testsHelper.devicesetup.DeviceSetup.deleteDownloadArtifactsFolder
import ch.protonmail.android.uitests.testsHelper.devicesetup.DeviceSetup.prepareArtifactsDir
import ch.protonmail.android.uitests.testsHelper.devicesetup.DeviceSetup.setupDevice
import ch.protonmail.android.uitests.testsHelper.testRail.TestRailService
import org.hamcrest.CoreMatchers.not
import me.proton.core.test.android.instrumented.CoreTest
import me.proton.core.test.android.instrumented.devicesetup.DeviceSetup.copyAssetFileToInternalFilesStorage
import me.proton.core.test.android.instrumented.devicesetup.DeviceSetup.deleteDownloadArtifactsFolder
import me.proton.core.test.android.instrumented.devicesetup.DeviceSetup.prepareArtifactsDir
import me.proton.core.test.android.instrumented.devicesetup.DeviceSetup.setupDevice
import org.junit.After
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestName
import org.junit.runner.RunWith
import kotlin.test.BeforeTest
@RunWith(AndroidJUnit4ClassRunner::class)
open class BaseTest {
open class BaseTest : CoreTest() {
private val activityRule = ActivityTestRule(LoginActivity::class.java)
@ -71,53 +59,31 @@ open class BaseTest {
.around(activityRule)!!
@BeforeTest
open fun setUp() {
Espresso.setFailureHandler(ProtonFailureHandler(InstrumentationRegistry.getInstrumentation()))
PreferenceManager.getDefaultSharedPreferences(targetContext).edit().clear().apply()
Intents.init()
clearLogcat()
Log.d(testTag, "Starting test execution for test: ${testName.methodName}")
// Show toast with test case name for better test analysis in recorded videos especially on Firebase.
override fun setUp() {
super.setUp()
InstrumentationRegistry.getInstrumentation().runOnMainSync {
Toast.makeText(targetContext, testName.methodName, tenSeconds).show()
}
Intents.intending(not(isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}
@After
open fun tearDown() {
Intents.release()
for (idlingResource in IdlingRegistry.getInstance().resources) {
if (idlingResource == null) {
continue
}
IdlingRegistry.getInstance().unregister(idlingResource)
}
override fun tearDown() {
super.tearDown()
device.removeWatcher("SystemDialogWatcher")
Log.d(testTag, "Finished test execution: ${testName.methodName}")
}
companion object {
val targetContext = InstrumentationRegistry.getInstrumentation().targetContext!!
val testContext = InstrumentationRegistry.getInstrumentation().context!!
var shouldReportToTestRail = false
val automation = InstrumentationRegistry.getInstrumentation().uiAutomation!!
val testName = TestName()
val artifactsPath = "${targetContext.filesDir.path}/artifacts"
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
const val testApp = "testApp"
const val testRailRunId = "testRailRunId"
const val testTag = "PROTON_UI_TEST"
const val downloadArtifactsPath = "/sdcard/Download/artifacts"
private val testExecutionWatcher = TestExecutionWatcher()
private const val reportToTestRail = "reportToTestRail"
private const val oneTimeRunFlag = "oneTimeRunFlag"
private const val email = 0
private const val password = 1
private const val mailboxPassword = 2
private const val twoFaKey = 3
private const val tenSeconds = 10000
private const val tenSeconds = 10_000
private val grantPermissionRule = GrantPermissionRule.grant(
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, READ_CONTACTS
)
@JvmStatic
@BeforeClass
@ -130,7 +96,7 @@ open class BaseTest {
// BeforeClass workaround for Android Test Orchestrator - shared prefs are not cleared
val isFirstRun = sharedPrefs.getBoolean(oneTimeRunFlag, true)
if (isFirstRun) {
setupDevice()
setupDevice(true)
prepareArtifactsDir(artifactsPath)
prepareArtifactsDir(downloadArtifactsPath)
deleteDownloadArtifactsFolder()
@ -165,10 +131,6 @@ open class BaseTest {
return User(userParams[email], userParams[password], userParams[mailboxPassword], userParams[twoFaKey])
}
private val grantPermissionRule = GrantPermissionRule.grant(
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, READ_CONTACTS
)
private fun copyAssetsToDownload() {
copyAssetFileToInternalFilesStorage("lorem_ipsum.docx")
copyAssetFileToInternalFilesStorage("lorem_ipsum.zip")

View File

@ -41,7 +41,7 @@ import ch.protonmail.android.uitests.testsHelper.TestData.pngFile
import ch.protonmail.android.uitests.testsHelper.TestData.twoPassUser
import ch.protonmail.android.uitests.testsHelper.TestData.zipFile
import ch.protonmail.android.uitests.testsHelper.annotations.TestId
import ch.protonmail.android.uitests.testsHelper.intentutils.MimeTypes
import me.proton.core.test.android.instrumented.intentutils.MimeTypes
import org.hamcrest.CoreMatchers.not
import kotlin.test.BeforeTest
import kotlin.test.Test

View File

@ -99,6 +99,13 @@ class AccountSettingsTests : BaseTest() {
.verify { mobileSignatureToggleCheckedStateIs(true) }
}
@Test
fun switchMobileSignatureToggleOn3() {
accountSettingsRobot
.privacy()
.enableRequestLinkConfirmation()
}
fun changeLoginPassword() {
accountSettingsRobot
.passwordManagement()

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.tests.settings
import android.app.Activity
import android.app.Instrumentation
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import ch.protonmail.android.uitests.robots.login.LoginRobot
import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot
import ch.protonmail.android.uitests.tests.BaseTest
import ch.protonmail.android.uitests.testsHelper.TestData
import org.hamcrest.CoreMatchers
import org.junit.Test
import kotlin.test.BeforeTest
class PrivacyAccountSettingsTests : BaseTest() {
private val loginRobot = LoginRobot()
private val accountSettingsRobot = AccountSettingsRobot()
@BeforeTest
override fun setUp() {
super.setUp()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
loginRobot
.loginTwoPasswordUser(TestData.twoPassUser)
.decryptMailbox(TestData.twoPassUser.mailboxPassword)
.menuDrawer()
.settings()
.openUserAccountSettings(TestData.twoPassUser)
}
@Test
fun enableAutoDownloadMessages() {
accountSettingsRobot
.privacy()
.autoDownloadMessages()
.enableAutoDownloadMessages()
.navigateUpToPrivacySettings()
.verify { autoDownloadImagesIsEnabled() }
}
@Test
fun enableBackgroundSync() {
accountSettingsRobot
.privacy()
.backgroundSync()
.enableBackgroundSync()
.navigateUpToPrivacySettings()
.verify { backgroundSyncIsEnabled() }
}
@Test
fun enableAutoShowRemoteImages() {
val messageSubject = "Fw: Plan your travel risk-free with Agoda. We've got you!"
val remoteContent = "Remote content"
accountSettingsRobot
.privacy()
.enableAutoShowRemoteImages()
.navigateUpToAccountSettings()
.navigateUpToSettings()
.navigateUpToInbox()
.menuDrawer()
.labelOrFolder(remoteContent)
.clickMessageBySubject(messageSubject)
.verify { showRemoteContentButtonIsGone() }
}
@Test
fun enableAutoShowEmbeddedImages() {
val messageSubject = "2020 Lifetime account auction and raffle, and new feature announcements"
val embeddedImages = "Embedded images"
accountSettingsRobot
.privacy()
.enableAutoShowEmbeddedImages()
.navigateUpToAccountSettings()
.navigateUpToSettings()
.navigateUpToInbox()
.menuDrawer()
.labelOrFolder(embeddedImages)
.clickMessageBySubject(messageSubject)
.verify { loadEmbeddedImagesButtonIsGone() }
}
@Test
fun enableAndDisablePreventTakingScreenshots() {
accountSettingsRobot
.privacy()
.enablePreventTakingScreenshots()
.disablePreventTakingScreenshots()
.verify { takingScreenshotIsDisabled() }
}
@Test
fun enableRequestLinkConfirmation() {
val folder = "Link confirmation"
val linkText = "www.wikipedia.org"
val messageTitle = "wiki"
accountSettingsRobot
.privacy()
.enableRequestLinkConfirmation()
.navigateUpToAccountSettings()
.navigateUpToSettings()
.navigateUpToInbox()
.menuDrawer()
.labelOrFolder(folder)
.clickMessageBySubject(messageTitle)
.clickLink(linkText)
.verify { linkIsPresentInDialogMessage(linkText) }
}
}

View File

@ -18,6 +18,7 @@
*/
package ch.protonmail.android.uitests.tests.suites
import ch.protonmail.android.uitests.tests.composer.AttachmentsTests
import ch.protonmail.android.uitests.tests.composer.ForwardMessageTests
import ch.protonmail.android.uitests.tests.composer.ReplyToMessageTests
import ch.protonmail.android.uitests.tests.composer.SendNewMessageTests
@ -25,19 +26,22 @@ import ch.protonmail.android.uitests.tests.contacts.ContactsTests
import ch.protonmail.android.uitests.tests.drafts.DraftsTests
import ch.protonmail.android.uitests.tests.inbox.InboxTests
import ch.protonmail.android.uitests.tests.inbox.SearchTests
import ch.protonmail.android.uitests.tests.labelsfolders.LabelsFoldersTests
import ch.protonmail.android.uitests.tests.login.LoginTests
import ch.protonmail.android.uitests.tests.manageaccounts.MultiuserManagementTests
import ch.protonmail.android.uitests.tests.menu.MenuTests
import ch.protonmail.android.uitests.tests.messagedetail.MessageDetailTests
import ch.protonmail.android.uitests.tests.settings.AccountSettingsTests
import ch.protonmail.android.uitests.tests.settings.PrivacyAccountSettingsTests
import ch.protonmail.android.uitests.tests.settings.SettingsTests
import ch.protonmail.android.uitests.tests.settings.SwipeGesturesTests
import org.junit.runner.RunWith
import org.junit.runners.Suite
@RunWith(Suite::class)
@Suite.SuiteClasses(
// Account settings tests
AccountSettingsTests::class,
// Composer tests
AttachmentsTests::class,
SendNewMessageTests::class,
ForwardMessageTests::class,
ReplyToMessageTests::class,
@ -47,15 +51,22 @@ import org.junit.runners.Suite
DraftsTests::class,
// Inbox tests
InboxTests::class,
// Labels and folders tests
LabelsFoldersTests::class,
// Login tests
LoginTests::class,
// Multi-user management tests
MultiuserManagementTests::class,
// Menu tests
MenuTests::class,
//Search tests
// Message detail tests
MessageDetailTests::class,
// Search tests
SearchTests::class,
// Settings tests
SettingsTests::class
AccountSettingsTests::class,
PrivacyAccountSettingsTests::class,
SettingsTests::class,
SwipeGesturesTests::class
)
class RegressionSuite

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.testsHelper
import android.app.Activity
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
/**
* Provides an activity which is in [Stage.RESUMED].
*/
object ActivityProvider {
val currentActivity: Activity? get() = getActivity()
private fun getActivity(): Activity? {
val currentActivity = arrayOfNulls<Activity>(1)
getInstrumentation().runOnMainSync {
val activities = ActivityLifecycleMonitorRegistry
.getInstance()
.getActivitiesInStage(Stage.RESUMED)
if (activities.iterator().hasNext()) {
currentActivity[0] = activities.iterator().next() as Activity
}
}
return currentActivity[0]
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.testsHelper
import android.app.Instrumentation
import android.view.View
import androidx.test.espresso.FailureHandler
import androidx.test.espresso.base.DefaultFailureHandler
import ch.protonmail.android.uitests.tests.BaseTest.Companion.artifactsPath
import ch.protonmail.android.uitests.tests.BaseTest.Companion.testName
import com.jraska.falcon.Falcon
import org.hamcrest.Matcher
import java.io.File
class ProtonFailureHandler(instrumentation: Instrumentation) : FailureHandler {
private val delegate: FailureHandler
init {
delegate = DefaultFailureHandler(instrumentation.targetContext)
}
override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
if (ProtonWatcher.status == ProtonWatcher.CONDITION_NOT_MET) {
// just delegate as we are in the condition check loop
delegate.handle(error, viewMatcher)
} else {
val file = File(artifactsPath, "${testName.methodName}-screenshot.png")
val activity = ActivityProvider.currentActivity
if (activity != null) {
Falcon.takeScreenshot(activity, file)
}
delegate.handle(error, viewMatcher)
}
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.testsHelper
class ProtonWatcher {
private var timeout = DEFAULT_TIMEOUT
private var watchInterval = DEFAULT_INTERVAL
abstract class Condition {
abstract fun getDescription(): String
abstract fun checkCondition(): Boolean
}
companion object {
const val CONDITION_NOT_MET = 0
const val DEFAULT_TIMEOUT = 10_000L
const val DEFAULT_INTERVAL = 250L
const val TIMEOUT = 2
var status = CONDITION_NOT_MET
private const val CONDITION_MET = 1
private val instance = ProtonWatcher()
fun waitForCondition(condition: Condition) {
// reset to initial state
status = 0
var timeInterval = 0L
while (status != CONDITION_MET) {
if (condition.checkCondition()) {
status = CONDITION_MET
break
} else {
if (timeInterval < instance.timeout) {
timeInterval += instance.watchInterval * 2
Thread.sleep(instance.watchInterval)
} else {
status = TIMEOUT
}
}
}
}
fun setTimeout(ms: Long) {
instance.timeout = ms
}
}
}

View File

@ -20,10 +20,13 @@ package ch.protonmail.android.uitests.testsHelper
import android.content.Context
import ch.protonmail.android.uitests.tests.BaseTest
import ch.protonmail.android.uitests.tests.BaseTest.Companion.artifactsPath
import ch.protonmail.android.uitests.tests.BaseTest.Companion.automation
import ch.protonmail.android.uitests.testsHelper.annotations.TestId
import ch.protonmail.android.uitests.testsHelper.testRail.TestRailService
import me.proton.core.test.android.instrumented.CoreTest.Companion.artifactsPath
import me.proton.core.test.android.instrumented.CoreTest.Companion.automation
import me.proton.core.test.android.instrumented.CoreTest.Companion.targetContext
import me.proton.core.test.android.instrumented.CoreTest.Companion.testApp
import me.proton.core.test.android.instrumented.CoreTest.Companion.testRailRunId
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import java.io.File
@ -57,6 +60,6 @@ class TestExecutionWatcher : TestWatcher() {
}
}
private fun getRunId(): String = BaseTest.targetContext.getSharedPreferences(BaseTest.testApp, Context.MODE_PRIVATE)
.getString(BaseTest.testRailRunId, "")!!
private fun getRunId(): String = targetContext.getSharedPreferences(testApp, Context.MODE_PRIVATE)
.getString(testRailRunId, "")!!
}

View File

@ -25,18 +25,8 @@ import android.widget.TextView
import androidx.annotation.IdRes
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.AmbiguousViewMatcherException
import androidx.test.espresso.AppNotIdleException
import androidx.test.espresso.Espresso
import androidx.test.espresso.NoMatchingRootException
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.PositionableRecyclerViewAction
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
@ -44,10 +34,11 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.platform.app.InstrumentationRegistry
import ch.protonmail.android.R
import ch.protonmail.android.contacts.list.listView.ContactsListAdapter
import ch.protonmail.android.uitests.testsHelper.ActivityProvider.currentActivity
import junit.framework.AssertionFailedError
import me.proton.core.test.android.instrumented.uiwaits.UIWaits.waitUntilLoaded
import me.proton.core.test.android.instrumented.utils.ActivityProvider.currentActivity
import me.proton.core.test.android.instrumented.watchers.ProtonWatcher
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matcher
import org.jetbrains.annotations.Contract
import kotlin.test.assertFalse
@ -59,102 +50,12 @@ object UICustomViewActions {
private const val TIMEOUT_5S = 5_000L
private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
fun waitUntilViewAppears(interaction: ViewInteraction, timeout: Long = TIMEOUT_10S): ViewInteraction {
val errorDescription = "UICustomViewActions.waitUntilViewAppears"
return waitUntilMatcherFulfilled(
interaction,
assertion = matches(isDisplayed()),
timeout = timeout,
errorDescription = errorDescription
)
}
fun waitUntilViewIsGone(interaction: ViewInteraction, timeout: Long = TIMEOUT_10S): ViewInteraction {
val errorDescription = "UICustomViewActions.waitUntilViewIsGone"
return waitUntilMatcherFulfilled(
interaction,
assertion = doesNotExist(),
timeout = timeout,
errorDescription = errorDescription
)
}
fun waitUntilViewIsNotDisplayed(interaction: ViewInteraction, timeout: Long = TIMEOUT_10S): ViewInteraction {
val errorDescription = "UICustomViewActions.waitUntilViewIsGone"
return waitUntilMatcherFulfilled(
interaction,
assertion = matches(not(isDisplayed())),
timeout = timeout,
errorDescription = errorDescription
)
}
fun waitUntilMatcherFulfilled(
interaction: ViewInteraction,
assertion: ViewAssertion,
timeout: Long = TIMEOUT_10S,
errorDescription: String = ""
): ViewInteraction {
ProtonWatcher.setTimeout(timeout)
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition() {
var errorMessage = ""
override fun getDescription() = "UICustomViewActions.waitUntilMatcherFulfilled $errorMessage"
override fun checkCondition(): Boolean {
return try {
interaction.check(assertion)
true
} catch (e: PerformException) {
errorMessage = "${e.viewDescription}, Action: ${e.actionDescription}"
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: NoMatchingViewException) {
errorMessage = e.viewMatcherDescription
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: NoMatchingRootException) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: AppNotIdleException) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: AmbiguousViewMatcherException) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: AssertionFailedError) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
}
}
})
return interaction
}
fun waitUntilIntentMatcherFulfilled(
matcher: Matcher<Intent>,
timeout: Long = TIMEOUT_5S
) {
ProtonWatcher.setTimeout(timeout)
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition() {
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition {
var errorMessage = ""
override fun getDescription() = "UICustomViewActions.waitUntilIntentMatcherFulfilled $errorMessage"
@ -174,90 +75,6 @@ object UICustomViewActions {
})
}
fun performActionWithRetry(
interaction: ViewInteraction,
action: ViewAction,
timeout: Long = TIMEOUT_10S
): ViewInteraction {
ProtonWatcher.setTimeout(timeout)
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition() {
var errorMessage = ""
override fun getDescription() = "UICustomViewActions.waitUntilMatcherFulfilled $errorMessage"
override fun checkCondition(): Boolean {
return try {
interaction.perform(action)
true
} catch (e: PerformException) {
errorMessage = "${e.viewDescription}, Action: ${e.actionDescription}"
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: NoMatchingViewException) {
errorMessage = e.viewMatcherDescription
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: NoMatchingRootException) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: AppNotIdleException) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: AmbiguousViewMatcherException) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
} catch (e: AssertionFailedError) {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw e
} else {
false
}
}
}
})
return interaction
}
fun waitUntilRecyclerViewPopulated(@IdRes id: Int, timeout: Long = TIMEOUT_10S) {
ProtonWatcher.setTimeout(timeout)
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition() {
override fun getDescription() =
"RecyclerView: ${targetContext.resources.getResourceName(id)} was not populated with items"
override fun checkCondition() = try {
val rv = currentActivity!!.findViewById<RecyclerView>(id)
if (rv != null) {
waitUntilLoaded { rv }
rv.adapter!!.itemCount > 0
} else {
if (ProtonWatcher.status == ProtonWatcher.TIMEOUT) {
throw Exception(getDescription())
} else {
false
}
}
} catch (e: Throwable) {
throw e
}
})
}
fun waitForAdapterItemWithIdAndText(
@IdRes recyclerViewId: Int,
@IdRes viewId: Int,
@ -265,7 +82,7 @@ object UICustomViewActions {
timeout: Long = 5000L
) {
ProtonWatcher.setTimeout(timeout)
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition() {
ProtonWatcher.waitForCondition(object : ProtonWatcher.Condition {
override fun getDescription() =
"RecyclerView: ${targetContext.resources.getResourceName(recyclerViewId)} was not populated with items"
@ -294,24 +111,6 @@ object UICustomViewActions {
})
}
/**
* Stop the test until RecyclerView's data gets loaded.
* Passed [recyclerProvider] will be activated in UI thread, allowing you to retrieve the View.
* Workaround for https://issuetracker.google.com/issues/123653014.
*/
inline fun waitUntilLoaded(crossinline recyclerProvider: () -> RecyclerView) {
Espresso.onIdle()
lateinit var recycler: RecyclerView
InstrumentationRegistry.getInstrumentation().runOnMainSync {
recycler = recyclerProvider()
}
while (recycler.hasPendingAdapterUpdates()) {
Thread.sleep(10)
}
}
@Contract(value = "_ -> new", pure = true)
fun setValueInNumberPicker(num: Int): ViewAction {
return object : ViewAction {

View File

@ -1,91 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.testsHelper.devicesetup
import androidx.test.platform.app.InstrumentationRegistry
import ch.protonmail.android.uitests.tests.BaseTest.Companion.automation
import ch.protonmail.android.uitests.tests.BaseTest.Companion.downloadArtifactsPath
import ch.protonmail.android.uitests.tests.BaseTest.Companion.targetContext
import java.io.File
import java.io.FileOutputStream
object DeviceSetup {
/**
* Sets up device in ready for automation mode.
* Animations turned off, long press timeout is set to 2 seconds, notifications popups are disabled.
*/
fun setupDevice() {
automation.executeShellCommand("settings put secure long_press_timeout 2000")
// Disable floating notification pop-ups.
automation.executeShellCommand("settings put global heads_up_notifications_enabled 0")
automation.executeShellCommand("settings put global animator_duration_scale 0.0")
automation.executeShellCommand("settings put global transition_animation_scale 0.0")
automation.executeShellCommand("settings put global window_animation_scale 0.0")
}
// Clears logcat log.
fun clearLogcat() {
automation.executeShellCommand("logcat -c")
}
// Deletes artifacts folder from /sdcard/Download.
fun deleteDownloadArtifactsFolder() {
automation.executeShellCommand("rm -rf $downloadArtifactsPath")
}
// Prepares artifacts directory in provided path.
fun prepareArtifactsDir(path: String) {
val dir = File(path)
if (!dir.exists()) {
dir.mkdirs()
} else {
if (dir.list() != null) {
dir.list().forEach { File(it).delete() }
}
}
}
/**
* Copies files from test project assets into main app files internal storage.
* @param fileName - name of the file which exists in androidTests/assets folder.
*/
fun copyAssetFileToInternalFilesStorage(fileName: String) {
val testContext = InstrumentationRegistry.getInstrumentation().context
val file = File("${targetContext.filesDir.path}/$fileName")
if (!file.exists()) {
try {
val inputStream = testContext.assets.open(fileName)
val fileOutputStream = FileOutputStream(file)
val size = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
inputStream.close()
fileOutputStream.write(buffer)
fileOutputStream.close()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
}

View File

@ -1,76 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.testsHelper.intentutils
import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import android.net.Uri
import androidx.test.platform.app.InstrumentationRegistry
import ch.protonmail.android.uitests.tests.BaseTest.Companion.automation
import java.io.File
import java.io.FileOutputStream
object IntentHelper {
fun sendShareFileIntent(mimeType: String, fileName: String) {
// val fileType = mimeType.split("/")[1]
// // Check if provided mime type corresponds to the file
// if (fileName.contains(fileType)) {
automation.executeShellCommand("am start -a android.intent.action.SEND -t $mimeType " +
"--eu android.intent.extra.STREAM " +
"file:///data/data/ch.protonmail.android.beta/files/$fileName " +
" --grant-read-uri-permission")
// } else {
// fail("Mime type:\"$mimeType\" doesn't correspond to the file:\"$fileName\"")
// }
}
// Creates new activity result for a file in test app assets.
fun createImageResultFromAssets(fileName: String): Instrumentation.ActivityResult {
val resultIntent = Intent()
// Declare variables for test and application context.
val testContext = InstrumentationRegistry.getInstrumentation().context
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val file = File("${appContext.cacheDir}/$fileName")
if (!file.exists()) {
try {
val inputStream = testContext.assets.open(fileName)
val fileOutputStream = FileOutputStream(file)
val size = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
inputStream.close()
fileOutputStream.write(buffer)
fileOutputStream.close()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
// Build a stubbed result from temp file.
resultIntent.data = Uri.fromFile(file)
return Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent)
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.uitests.testsHelper.intentutils
object MimeTypes {
val application = Application
val text = Text
val image = Image
val video = Video
object Application {
val pdf = "application/pdf"
val zip = "application/zip"
val docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
object Text {
val plain = "text/plain"
val rtf = "text/rtf"
val html = "text/html"
val json = "text/json"
}
object Image {
val png = "image/png"
val jpeg = "image/jpeg"
val gif = "image/gif"
}
object Video {
val mp4 = "video/mp4"
val jp3 = "video/3gp"
}
}

View File

@ -20,7 +20,7 @@ package ch.protonmail.android.uitests.testsHelper.testRail
import android.util.Log
import ch.protonmail.android.beta.test.BuildConfig
import ch.protonmail.android.uitests.tests.BaseTest.Companion.testTag
import me.proton.core.test.android.instrumented.CoreTest.Companion.testTag
import org.json.simple.JSONObject
import java.io.IOException
import java.util.HashMap

View File

@ -26,7 +26,6 @@ import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@ -83,6 +82,9 @@ object Check {
fun alertDialogWithTextIsDisplayed(@StringRes textId: Int): ViewInteraction =
onView(withText(textId)).inRoot(RootMatchers.isDialog()).check(matches(isDisplayed()))
fun alertDialogWithPartialTextIsDisplayed(text: String): ViewInteraction =
onView(withText(containsString(text))).inRoot(RootMatchers.isDialog()).check(matches(isDisplayed()))
fun viewWithTextIsChecked(@StringRes textId: Int): ViewInteraction =
onView(withText(textId)).inRoot(RootMatchers.isDialog()).check(matches(isChecked()))
}

View File

@ -39,10 +39,10 @@ import ch.protonmail.android.uitests.robots.manageaccounts.ManageAccountsMatcher
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.checkContactDoesNotExist
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.checkMessageDoesNotExist
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.clickOnChildWithId
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.performActionWithRetry
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.saveMessageSubject
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitForAdapterItemWithIdAndText
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilRecyclerViewPopulated
import me.proton.core.test.android.instrumented.uiwaits.UIWaits.performActionWithRetry
import me.proton.core.test.android.instrumented.uiwaits.UIWaits.waitUntilRecyclerViewPopulated
import org.hamcrest.Matcher
object Recycler {

View File

@ -24,7 +24,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import ch.protonmail.android.uitests.tests.BaseTest.Companion.targetContext
import me.proton.core.test.android.instrumented.CoreTest.Companion.targetContext
import org.hamcrest.CoreMatchers.`is`
object Tag {

View File

@ -26,6 +26,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -33,15 +34,18 @@ import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import ch.protonmail.android.uitests.tests.BaseTest.Companion.device
import ch.protonmail.android.uitests.testsHelper.StringUtils
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilMatcherFulfilled
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewAppears
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewIsGone
import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilIntentMatcherFulfilled
import me.proton.core.test.android.instrumented.CoreTest.Companion.device
import me.proton.core.test.android.instrumented.uiwaits.UIWaits
import me.proton.core.test.android.instrumented.uiwaits.UIWaits.waitForView
import me.proton.core.test.android.instrumented.uiwaits.UIWaits.waitUntilMatcherFulfilled
import me.proton.core.test.android.instrumented.uiwaits.UIWaits.waitUntilViewIsGone
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matcher
import org.junit.Assert
@ -52,22 +56,22 @@ object Wait {
}
fun forViewByViewInteraction(interaction: ViewInteraction): ViewInteraction =
waitUntilViewAppears(interaction)
waitForView(interaction)
fun forViewWithContentDescription(@StringRes textId: Int): ViewInteraction =
waitUntilViewAppears(onView(withContentDescription(containsString(StringUtils.stringFromResource(textId)))))
waitForView(onView(withContentDescription(containsString(StringUtils.stringFromResource(textId)))))
fun forViewWithId(@IdRes id: Int, timeout: Long = 10_000L): ViewInteraction =
waitUntilViewAppears(onView(withId(id)), timeout)
waitForView(onView(withId(id)), timeout)
fun forViewWithIdAndText(@IdRes id: Int, text: String): ViewInteraction =
waitUntilViewAppears(onView(allOf(withId(id), withText(text))))
waitForView(onView(allOf(withId(id), withText(text))))
fun forViewWithIdAndText(@IdRes id: Int, textId: Int, timeout: Long = 5000): ViewInteraction =
waitUntilViewAppears(onView(allOf(withId(id), withText(textId))), timeout)
waitForView(onView(allOf(withId(id), withText(textId))), timeout)
fun forViewWithIdAndParentId(@IdRes id: Int, @IdRes parentId: Int): ViewInteraction =
waitUntilViewAppears(onView(allOf(withId(id), withParent(withId(parentId)))))
waitForView(onView(allOf(withId(id), withParent(withId(parentId)))))
fun untilViewWithIdDisabled(@IdRes id: Int): ViewInteraction =
waitUntilMatcherFulfilled(onView(withId(id)), matches(isEnabled()))
@ -76,19 +80,19 @@ object Wait {
waitUntilViewIsGone(onView(withId(id)))
fun forViewOfInstanceWithParentId(@IdRes id: Int, clazz: Class<*>, timeout: Long = 5000): ViewInteraction =
waitUntilViewAppears(onView(allOf(instanceOf(clazz), withParent(withId(id)))), timeout)
waitForView(onView(allOf(instanceOf(clazz), withParent(withId(id)))), timeout)
fun forViewWithText(@StringRes textId: Int): ViewInteraction =
waitUntilViewAppears(onView(withText(StringUtils.stringFromResource(textId))))
waitForView(onView(withText(StringUtils.stringFromResource(textId))))
fun forViewWithText(text: String): ViewInteraction =
waitUntilViewAppears(onView(withText(text)))
waitForView(onView(withText(text)))
fun forViewWithTextAndParentId(@StringRes text: Int, @IdRes parentId: Int): ViewInteraction =
waitUntilViewAppears(onView(allOf(withText(text), withParent(withId(parentId)))))
waitForView(onView(allOf(withText(text), withParent(withId(parentId)))))
fun forViewWithTextAndParentId(text: String, @IdRes parentId: Int): ViewInteraction =
waitUntilViewAppears(onView(allOf(withText(text), withParent(withId(parentId)))))
waitForView(onView(allOf(withText(text), withParent(withId(parentId)))))
fun untilViewWithIdEnabled(@IdRes id: Int): ViewInteraction =
waitUntilMatcherFulfilled(onView(withId(id)), matches(isEnabled()))
@ -103,10 +107,10 @@ object Wait {
waitUntilViewIsGone(onView(withText(text)))
fun forViewWithIdAndAncestorId(@IdRes id: Int, @IdRes parentId: Int): ViewInteraction =
waitUntilViewAppears(onView(allOf(withId(id), ViewMatchers.isDescendantOfA(withId(parentId)))))
waitForView(onView(allOf(withId(id), ViewMatchers.isDescendantOfA(withId(parentId)))))
fun untilViewWithIdIsNotShown(@IdRes id: Int): ViewInteraction =
UICustomViewActions.waitUntilViewIsNotDisplayed(onView(withId(id)))
waitUntilMatcherFulfilled(onView(withId(id)), matches(not(isDisplayed())))
fun forIntent(matcher: Matcher<Intent>) = UICustomViewActions.waitUntilIntentMatcherFulfilled(matcher)
fun forIntent(matcher: Matcher<Intent>) = waitUntilIntentMatcherFulfilled(matcher)
}

View File

@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/*
* Copyright (c) 2020 Proton Technologies AG
*
@ -18,12 +20,14 @@
*/
plugins {
`kotlin-dsl`
kotlin("jvm") version "1.4.30-RC"
}
repositories {
google()
jcenter()
maven(url = "https://dl.bintray.com/proton/Core-publishing")
maven("https://dl.bintray.com/kotlin/kotlin-eap")
}
dependencies {
@ -34,4 +38,13 @@ dependencies {
implementation("com.android.tools.build:gradle:$android")
// Needed for many utils
implementation("studio.forface.easygradle:dsl-android:$easyGradle")
implementation(kotlin("stdlib-jdk8"))
}
val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
jvmTarget = "1.8"
}
val compileTestKotlin: KotlinCompile by tasks
compileTestKotlin.kotlinOptions {
jvmTarget = "1.8"
}

View File

@ -102,6 +102,7 @@ val DependencyHandler.`remark` get() = dependency("com.over
val DependencyHandler.aerogear get() = dependency("org.jboss.aerogear", module = "aerogear-otp-java") version `aerogear version`
val DependencyHandler.`espresso-contrib` get() = androidx("test.espresso", module = "espresso-contrib") version `espresso version`
val DependencyHandler.`espresso-intents` get() = androidx("test.espresso", module = "espresso-intents") version `espresso version`
val DependencyHandler.`espresso-web` get() = androidx("test.espresso", module = "espresso-web") version `espresso version`
val DependencyHandler.falcon get() = dependency("com.jraska", module = "falcon") version `falcon version`
val DependencyHandler.`orchestrator` get() = androidx("test", module = "orchestrator") version `android-test version`
val DependencyHandler.`browserstack-gradle-plugin` get() = dependency("gradle.plugin.com.browserstack.gradle", module = "browserstack-gradle-plugin") version `browserstack-plugin version`

View File

@ -68,7 +68,7 @@ const val `Proton-shared-preferences version` = "0.2.3" // Released: Dec
const val `Proton-work-manager version` = "0.2.2" // Released: Dec 18, 2020
// Test
const val `Proton-android-test version` = "0.3.3" // Released: Dec 18, 2020
const val `Proton-android-instr-test version` = "0.2.2" // Released: Dec 18, 2020
const val `Proton-android-instr-test version` = "0.3.2" // Released: Jan 08, 2020
const val `Proton-kotlin-test version` = "0.2" // Released: Oct 21, 2020
const val `Proton-domain version` = "0.2.4" // Released: Nov 18, 2020