From 815c73944f02d14bd9689325ad243f491d2fc526 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 9 Jun 2025 16:06:36 -0300 Subject: [PATCH 01/37] Creating base e2e test for Real Device validations --- .../androidTest/kotlin/RealDeviceE2ETests.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/src/androidTest/kotlin/RealDeviceE2ETests.kt diff --git a/app/src/androidTest/kotlin/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/RealDeviceE2ETests.kt new file mode 100644 index 00000000000..56c80f41bab --- /dev/null +++ b/app/src/androidTest/kotlin/RealDeviceE2ETests.kt @@ -0,0 +1,94 @@ +package com.x8bit.bitwarden.e2e + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.x8bit.bitwarden.MainActivity +import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RealDeviceE2ETests { + + @Before + fun setup() { + // Clear any existing state + InstrumentationRegistry.getInstrumentation().targetContext.packageManager + .clearPackagePreferredActivities(InstrumentationRegistry.getInstrumentation().targetContext.packageName) + } + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun testVaultLockUnlockFlow() { + // 1. Update environment URL to test.com + composeTestRule.onNodeWithTag("ChooseLoginButton") + .performClick() + + composeTestRule.onNodeWithTag("RegionSelectorDropdown") + .performClick() + + composeTestRule.onNodeWithTag("ServerUrlEntry") + .performClick() + .performTextInput("test.com") + + composeTestRule.onNodeWithTag("SaveButton") + .performClick() + + // Wait for save to complete + composeTestRule.waitUntil(timeoutMillis = 5000) { + composeTestRule.onAllNodesWithTag("EmailEntry").fetchSemanticsNodes().isNotEmpty() + } + + // 2. Login with test credentials + composeTestRule.onNodeWithTag("EmailEntry") + .performClick() + .performTextInput("test@bitwarden.com") + + composeTestRule.onNodeWithTag("MasterPasswordEntry") + .performClick() + .performTextInput("password123") + + composeTestRule.onNodeWithTag("LoginButton") + .performClick() + + // Wait for login to complete and verify we're logged in + composeTestRule.waitUntil(timeoutMillis = 5000) { + composeTestRule.onAllNodesWithTag("SettingsButton").fetchSemanticsNodes().isNotEmpty() + } + + // 3. Go to settings and lock vault + composeTestRule.onNodeWithTag("SettingsButton") + .performClick() + + composeTestRule.onNodeWithTag("LockNowButton") + .performClick() + + // Wait for vault to lock + composeTestRule.waitUntil(timeoutMillis = 5000) { + composeTestRule.onAllNodesWithTag("MasterPasswordEntry").fetchSemanticsNodes().isNotEmpty() + } + + // 4. Unlock vault + composeTestRule.onNodeWithTag("MasterPasswordEntry") + .performClick() + .performTextInput("password123") + + composeTestRule.onNodeWithTag("UnlockButton") + .performClick() + + // 5. Verify vault is unlocked + composeTestRule.waitUntil(timeoutMillis = 5000) { + composeTestRule.onAllNodesWithTag("SettingsButton").fetchSemanticsNodes().isNotEmpty() + } + + // Additional verification + composeTestRule.onNodeWithTag("SettingsButton") + .assertIsDisplayed() + } +} From ed5b752717a4caa55873ad30b2e6760944764742 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 9 Jun 2025 16:09:17 -0300 Subject: [PATCH 02/37] Creating base e2e test for Real Device validations --- .../androidTest/kotlin/RealDeviceE2ETests.kt | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/app/src/androidTest/kotlin/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/RealDeviceE2ETests.kt index 56c80f41bab..2ab5cd42378 100644 --- a/app/src/androidTest/kotlin/RealDeviceE2ETests.kt +++ b/app/src/androidTest/kotlin/RealDeviceE2ETests.kt @@ -1,12 +1,9 @@ -package com.x8bit.bitwarden.e2e - +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import com.x8bit.bitwarden.MainActivity -import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest -import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -14,13 +11,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RealDeviceE2ETests { - @Before - fun setup() { - // Clear any existing state - InstrumentationRegistry.getInstrumentation().targetContext.packageManager - .clearPackagePreferredActivities(InstrumentationRegistry.getInstrumentation().targetContext.packageName) - } - @get:Rule val composeTestRule = createAndroidComposeRule() @@ -33,62 +23,72 @@ class RealDeviceE2ETests { composeTestRule.onNodeWithTag("RegionSelectorDropdown") .performClick() + var performClick = composeTestRule.onAllNodes(hasTestTag("AlertRadioButtonOption")) + .get(2) + .performClick() + composeTestRule.onNodeWithTag("ServerUrlEntry") .performClick() - .performTextInput("test.com") + .performTextInput("- - - - - -- ") composeTestRule.onNodeWithTag("SaveButton") .performClick() // Wait for save to complete composeTestRule.waitUntil(timeoutMillis = 5000) { - composeTestRule.onAllNodesWithTag("EmailEntry").fetchSemanticsNodes().isNotEmpty() + composeTestRule.onAllNodesWithTag("ServerUrlEntry").fetchSemanticsNodes().isEmpty() } // 2. Login with test credentials - composeTestRule.onNodeWithTag("EmailEntry") + composeTestRule.onNodeWithTag("EmailAddressEntry") + .performClick() + .performTextInput("- - - - - - -") + + composeTestRule.onNodeWithTag("ContinueButton") .performClick() - .performTextInput("test@bitwarden.com") composeTestRule.onNodeWithTag("MasterPasswordEntry") .performClick() - .performTextInput("password123") + .performTextInput("- - - - - - -") - composeTestRule.onNodeWithTag("LoginButton") + composeTestRule.onNodeWithTag("LogInWithMasterPasswordButton") .performClick() // Wait for login to complete and verify we're logged in - composeTestRule.waitUntil(timeoutMillis = 5000) { - composeTestRule.onAllNodesWithTag("SettingsButton").fetchSemanticsNodes().isNotEmpty() + composeTestRule.waitUntil(timeoutMillis = 20000) { + composeTestRule.onAllNodesWithTag("SettingsTab").fetchSemanticsNodes().isNotEmpty() } // 3. Go to settings and lock vault - composeTestRule.onNodeWithTag("SettingsButton") + composeTestRule.onNodeWithTag("SettingsTab") + .performClick() + + composeTestRule.onNodeWithTag("AccountSecuritySettingsButton") .performClick() - composeTestRule.onNodeWithTag("LockNowButton") + composeTestRule.onNodeWithTag("LockNowLabel").performScrollTo() .performClick() // Wait for vault to lock - composeTestRule.waitUntil(timeoutMillis = 5000) { + composeTestRule.waitUntil(timeoutMillis = 20000) { composeTestRule.onAllNodesWithTag("MasterPasswordEntry").fetchSemanticsNodes().isNotEmpty() } // 4. Unlock vault composeTestRule.onNodeWithTag("MasterPasswordEntry") .performClick() - .performTextInput("password123") + .performTextInput("- - - - - - -") - composeTestRule.onNodeWithTag("UnlockButton") + composeTestRule.onNodeWithTag("UnlockVaultButton") .performClick() // 5. Verify vault is unlocked - composeTestRule.waitUntil(timeoutMillis = 5000) { - composeTestRule.onAllNodesWithTag("SettingsButton").fetchSemanticsNodes().isNotEmpty() + composeTestRule.waitUntil(timeoutMillis = 20000) { + composeTestRule.onAllNodesWithTag("SettingsTab").fetchSemanticsNodes().isNotEmpty() } // Additional verification - composeTestRule.onNodeWithTag("SettingsButton") + composeTestRule.onNodeWithTag("SettingsTab") .assertIsDisplayed() } } From 5b012e1d2318da210f8054be721111db28e076a6 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Tue, 10 Jun 2025 14:23:21 -0300 Subject: [PATCH 03/37] Implementing page object pattern --- app/build.gradle.kts | 4 + .../androidTest/kotlin/RealDeviceE2ETests.kt | 94 -------------- .../kotlin/e2e/pageObjects/Page.kt | 122 ++++++++++++++++++ .../login/EnvironmentSettingsPage.kt | 21 +++ .../kotlin/e2e/pageObjects/login/LoginPage.kt | 47 +++++++ .../kotlin/e2e/pageObjects/login/MainPage.kt | 20 +++ .../e2e/pageObjects/settings/SettingsPage.kt | 21 +++ .../accountSecurity/AccountSecurityPage.kt | 25 ++++ .../e2e/pageObjects/vault/UnlockVaultPage.kt | 27 ++++ .../kotlin/e2e/pageObjects/vault/VaultPage.kt | 26 ++++ .../kotlin/e2e/tests/RealDeviceE2ETests.kt | 31 +++++ gradle/libs.versions.toml | 8 ++ 12 files changed, 352 insertions(+), 94 deletions(-) delete mode 100644 app/src/androidTest/kotlin/RealDeviceE2ETests.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/Page.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt create mode 100644 app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c9ed702efbb..9607278d475 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -241,6 +241,10 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.uiautomator) + implementation(libs.androidx.espresso.core) + implementation(libs.androidx.junit.ktx) + implementation(libs.androidx.ui.test.junit4.android) ksp(libs.androidx.room.compiler) implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.runtime) diff --git a/app/src/androidTest/kotlin/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/RealDeviceE2ETests.kt deleted file mode 100644 index 2ab5cd42378..00000000000 --- a/app/src/androidTest/kotlin/RealDeviceE2ETests.kt +++ /dev/null @@ -1,94 +0,0 @@ -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.semantics.getOrNull -import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.x8bit.bitwarden.MainActivity -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RealDeviceE2ETests { - - @get:Rule - val composeTestRule = createAndroidComposeRule() - - @Test - fun testVaultLockUnlockFlow() { - // 1. Update environment URL to test.com - composeTestRule.onNodeWithTag("ChooseLoginButton") - .performClick() - - composeTestRule.onNodeWithTag("RegionSelectorDropdown") - .performClick() - - var performClick = composeTestRule.onAllNodes(hasTestTag("AlertRadioButtonOption")) - .get(2) - .performClick() - - composeTestRule.onNodeWithTag("ServerUrlEntry") - .performClick() - .performTextInput("- - - - - -- ") - - composeTestRule.onNodeWithTag("SaveButton") - .performClick() - - // Wait for save to complete - composeTestRule.waitUntil(timeoutMillis = 5000) { - composeTestRule.onAllNodesWithTag("ServerUrlEntry").fetchSemanticsNodes().isEmpty() - } - - // 2. Login with test credentials - composeTestRule.onNodeWithTag("EmailAddressEntry") - .performClick() - .performTextInput("- - - - - - -") - - composeTestRule.onNodeWithTag("ContinueButton") - .performClick() - - composeTestRule.onNodeWithTag("MasterPasswordEntry") - .performClick() - .performTextInput("- - - - - - -") - - composeTestRule.onNodeWithTag("LogInWithMasterPasswordButton") - .performClick() - - // Wait for login to complete and verify we're logged in - composeTestRule.waitUntil(timeoutMillis = 20000) { - composeTestRule.onAllNodesWithTag("SettingsTab").fetchSemanticsNodes().isNotEmpty() - } - - // 3. Go to settings and lock vault - composeTestRule.onNodeWithTag("SettingsTab") - .performClick() - - composeTestRule.onNodeWithTag("AccountSecuritySettingsButton") - .performClick() - - composeTestRule.onNodeWithTag("LockNowLabel").performScrollTo() - .performClick() - - // Wait for vault to lock - composeTestRule.waitUntil(timeoutMillis = 20000) { - composeTestRule.onAllNodesWithTag("MasterPasswordEntry").fetchSemanticsNodes().isNotEmpty() - } - - // 4. Unlock vault - composeTestRule.onNodeWithTag("MasterPasswordEntry") - .performClick() - .performTextInput("- - - - - - -") - - composeTestRule.onNodeWithTag("UnlockVaultButton") - .performClick() - - // 5. Verify vault is unlocked - composeTestRule.waitUntil(timeoutMillis = 20000) { - composeTestRule.onAllNodesWithTag("SettingsTab").fetchSemanticsNodes().isNotEmpty() - } - - // Additional verification - composeTestRule.onNodeWithTag("SettingsTab") - .assertIsDisplayed() - } -} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt new file mode 100644 index 00000000000..ab3ffc14185 --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -0,0 +1,122 @@ +package e2e.pageObjects + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + +/** + * Base class for all page objects in the Bitwarden app. + * Provides a shared ComposeTestRule instance for UI testing. + */ +abstract class Page(protected val composeTestRule: ComposeTestRule) { + companion object { + val TIMEOUT_MILLIS = 15000L + } + + /** + * Waits for an element with the specified test tag to be present and returns its SemanticsNodeInteraction. + * @param testTag The test tag of the element to wait for + * @return SemanticsNodeInteraction for the found element + * @throws AssertionError if the element is not found within the timeout period + */ + protected fun getElement(testTag: String): SemanticsNodeInteraction { + waitForIdle() + waitUntil(TIMEOUT_MILLIS) { + try { + composeTestRule.onNodeWithTag(testTag).assertExists() + true + } catch (e: AssertionError) { + false + } + } + return composeTestRule.onNodeWithTag(testTag) + } + + protected fun getElementByText(text: String): SemanticsNodeInteraction{ + waitForIdle() + waitUntil(TIMEOUT_MILLIS) { + try { + composeTestRule.onNodeWithText(text).assertExists() + true + } catch (e: AssertionError) { + false + } + } + return composeTestRule.onNodeWithText(text) + } + + /** + * Waits for the app to be idle before proceeding with any UI interactions. + * This helps prevent flaky tests by ensuring the UI is stable. + */ + protected fun waitForIdle() { + composeTestRule.waitForIdle() + } + + /** + * Waits for a specific condition to be true before proceeding. + * @param timeoutMillis Maximum time to wait in milliseconds + * @param condition The condition to wait for + */ + protected fun waitUntil( + timeoutMillis: Long, + condition: () -> Boolean + ) { + composeTestRule.waitUntil(timeoutMillis) { condition() } + } + + /** + * Performs a click action on a node with the given test tag. + * @param testTag The test tag of the node to click + */ + protected fun clickOnNodeWithTag(testTag: String) { + getElement(testTag).performClick() + } + + /** + * Verifies that a node with the given test tag is displayed. + * @param testTag The test tag of the node to verify + */ + protected fun verifyNodeWithTagIsDisplayed(testTag: String) { + getElement(testTag).assertIsDisplayed() + } + + /** + * Verifies that a node with the given test tag is not displayed. + * @param testTag The test tag of the node to verify + */ + protected fun verifyNodeWithTagIsNotDisplayed(testTag: String) { + composeTestRule.onNodeWithTag(testTag).assertDoesNotExist() + } + + /** + * Verifies that a node with the given test tag is enabled. + * @param testTag The test tag of the node to verify + */ + protected fun verifyNodeWithTagIsEnabled(testTag: String) { + getElement(testTag).assertIsEnabled() + } + + /** + * Verifies that a node with the given test tag is disabled. + * @param testTag The test tag of the node to verify + */ + protected fun verifyNodeWithTagIsDisabled(testTag: String) { + getElement(testTag).assertIsNotEnabled() + } + + /** + * Verifies that a node with the given test tag has the expected text. + * @param testTag The test tag of the node to verify + * @param expectedText The expected text content + */ + protected fun verifyNodeWithTagHasText(testTag: String, expectedText: String) { + getElement(testTag).assertTextEquals(expectedText) + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt new file mode 100644 index 00000000000..556f1c3713d --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt @@ -0,0 +1,21 @@ +package e2e.pageObjects.login + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import com.x8bit.bitwarden.e2e.pages.LoginPage +import e2e.pageObjects.Page + +class EnvironmentSettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ + + private val serverURLField by lazy { getElement("ServerUrlEntry") } + private val saveButton by lazy { getElement("SaveButton") } + + fun setupEnvironment(url: String) : LoginPage { + serverURLField + .performClick() + .performTextInput(url) + saveButton.performClick() + return LoginPage(composeTestRule) + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt new file mode 100644 index 00000000000..d4f5256c4ba --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt @@ -0,0 +1,47 @@ +package com.x8bit.bitwarden.e2e.pages + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.ComposeTestRule +import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData +import e2e.pageObjects.Page +import e2e.pageObjects.login.EnvironmentSettingsPage +import e2e.pageObjects.vault.VaultPage + +/** + * Page Object representing the Login screen of the Bitwarden app. + * This class encapsulates all the UI elements and actions available on the login screen. + */ +class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ + + // UI Elements + private val emailField by lazy { getElement("EmailAddressEntry") } + private val masterPasswordField by lazy { getElement("MasterPasswordEntry") } + private val continueButton by lazy { getElement("ContinueButton") } + private val loginWithMasterPasswordButton by lazy { getElement("LogInWithMasterPasswordButton") } + private val regionSelectorButton by lazy { getElement("RegionSelectorDropdown") } + + /** + * Enters the master password in the password field + * @param password The master password to enter + * @return This LoginPage instance for method chaining + */ + fun performLogin(email: String, password: String): VaultPage { + emailField + .performClick() + .performTextInput(email) + continueButton + .performClick() + masterPasswordField + .performClick() + .performTextInput(password) + loginWithMasterPasswordButton.performClick() + return VaultPage(composeTestRule) + } + + fun openEnvironmentSettings() : EnvironmentSettingsPage { + regionSelectorButton.performClick() + getElementByText("Self-hosted") + .performClick() + return EnvironmentSettingsPage(composeTestRule) + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt new file mode 100644 index 00000000000..9520a7dea29 --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt @@ -0,0 +1,20 @@ +package e2e.pageObjects.login + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import com.x8bit.bitwarden.e2e.pages.LoginPage +import e2e.pageObjects.Page +import e2e.pageObjects.vault.VaultPage + +class MainPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { + + // UI Elements + private val loginButton by lazy { getElement("ChooseLoginButton") } + private val createAccountButton by lazy { getElement("ChooseAccountCreationButton") } + + fun startLogin(): LoginPage { + loginButton.performClick() + return LoginPage(composeTestRule) + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt new file mode 100644 index 00000000000..50a5312fc02 --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt @@ -0,0 +1,21 @@ +package e2e.pageObjects.settings + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.* +import e2e.pageObjects.Page +import e2e.pageObjects.settings.accountSecurity.AccountSecurityPage + +class SettingsPage (composeTestRule: ComposeTestRule) : Page(composeTestRule) { + + // UI Elements + private val accountSecurityButton by lazy { getElement("AccountSecuritySettingsButton") } + + /** + * Navigates to the Account Security settings + * @return This SettingsPage instance for method chaining + */ + fun navigateToAccountSecurity(): AccountSecurityPage { + accountSecurityButton.performClick() + return AccountSecurityPage(composeTestRule) + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt new file mode 100644 index 00000000000..2b468b5d123 --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt @@ -0,0 +1,25 @@ +package e2e.pageObjects.settings.accountSecurity + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.ComposeTestRule +import e2e.pageObjects.Page +import e2e.pageObjects.vault.UnlockVaultPage + +/** + * Page Object representing the Account Security screen of the Bitwarden app. + * This class encapsulates all the UI elements and actions available on the account security screen. + */ +class AccountSecurityPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { + + // UI Elements + private val lockNowLabel by lazy { getElement("LockNowLabel") } + + /** + * Locks the vault + * @return This AccountSecurityPage instance for method chaining + */ + fun lockVault(): UnlockVaultPage { + lockNowLabel.performScrollTo().performClick() + return UnlockVaultPage(composeTestRule) + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt new file mode 100644 index 00000000000..8b65db3cd08 --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt @@ -0,0 +1,27 @@ +package e2e.pageObjects.vault + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import e2e.pageObjects.Page + +class UnlockVaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { + + // UI Elements + private val passwordEntryTag by lazy { getElement("MasterPasswordEntry") } + private val unlockVaultButtonTag by lazy { getElement("UnlockVaultButton") } + + + fun enterPassword(password: String): UnlockVaultPage { + passwordEntryTag.performTextInput(password) + return this + } + + fun performUnlockVault(password: String): VaultPage { + passwordEntryTag.performClick().performTextInput(password) + unlockVaultButtonTag.performClick() + return VaultPage(composeTestRule) + } + +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt new file mode 100644 index 00000000000..fd701ad9ce6 --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt @@ -0,0 +1,26 @@ +package e2e.pageObjects.vault + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import e2e.pageObjects.Page +import e2e.pageObjects.settings.SettingsPage + +class VaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { + + // UI Elements + private val settingsMenuButton by lazy { getElement("SettingsTab") } + private val addItemButton by lazy { getElement("AddItemButton") } + + fun assertVaultIsUnlocked() { + addItemButton.assertIsDisplayed() + } + + fun navigateToSettingsPage(): SettingsPage { + settingsMenuButton.performClick() + return SettingsPage(composeTestRule) + } +} diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt new file mode 100644 index 00000000000..55a8dc517ce --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt @@ -0,0 +1,31 @@ +package e2e.tests + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.x8bit.bitwarden.MainActivity +import e2e.pageObjects.login.MainPage +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RealDeviceE2ETests { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun testVaultLockUnlockFlow() { + MainPage(composeTestRule) + .startLogin() + .openEnvironmentSettings() + .setupEnvironment("-") + .performLogin("-", "-") + .navigateToSettingsPage() + .navigateToAccountSecurity() + .lockVault() + .performUnlockVault("-") + .assertVaultIsUnlocked() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd932b87ea6..171bd596e4c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,10 @@ testng = "7.11.0" timber = "5.0.1" turbine = "1.2.0" zxing = "3.5.3" +uiautomator = "2.3.0" +espressoCore = "3.6.1" +junitKtx = "1.2.1" +uiTestJunit4Android = "1.8.2" [libraries] # Format: - @@ -126,6 +130,10 @@ square-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } zxing-zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" } testng = { group = "org.testng", name = "testng", version.ref = "testng" } +androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" } +androidx-ui-test-junit4-android = { group = "androidx.compose.ui", name = "ui-test-junit4-android", version.ref = "uiTestJunit4Android" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From a6ef2ea78dd19df746a54d66c36315546d8b6e43 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Thu, 26 Jun 2025 10:34:59 -0300 Subject: [PATCH 04/37] Using Compose Testing + Espresso --- app/build.gradle.kts | 6 ++++++ app/proguard-rules.pro | 11 +++++++++++ app/src/androidTest/kotlin/e2e/pageObjects/Page.kt | 2 +- .../kotlin/e2e/tests/RealDeviceE2ETests.kt | 9 ++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9607278d475..c8f9844c9b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -293,6 +293,12 @@ dependencies { testImplementation(libs.robolectric.robolectric) testImplementation(libs.square.okhttp.mockwebserver) testImplementation(libs.square.turbine) + androidTestImplementation(libs.androidx.uiautomator) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.junit.ktx) + androidTestImplementation(libs.androidx.ui.test.junit4.android) + androidTestImplementation("androidx.test:runner:1.6.0") + androidTestImplementation("androidx.test:rules:1.6.0") } tasks { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index d4963ef3c0f..b291901c0c7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -121,3 +121,14 @@ -dontwarn com.google.errorprone.annotations.CheckReturnValue -dontwarn com.google.errorprone.annotations.Immutable -dontwarn com.google.errorprone.annotations.RestrictedApi + +################################################################################ +# AndroidX Test Runner +################################################################################ + +# Keep the test runner classes +-keep class androidx.test.runner.** { *; } +-keep class androidx.test.internal.runner.** { *; } +-keep class androidx.test.ext.junit.** { *; } +-keep class androidx.test.ext.** { *; } +-keep class androidx.test.** { *; } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt index ab3ffc14185..53d43f0f9c3 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.test.performClick */ abstract class Page(protected val composeTestRule: ComposeTestRule) { companion object { - val TIMEOUT_MILLIS = 15000L + val TIMEOUT_MILLIS = 30000L } /** diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt index 55a8dc517ce..5a728cc3a67 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt @@ -1,7 +1,10 @@ package e2e.tests import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.x8bit.bitwarden.MainActivity import e2e.pageObjects.login.MainPage @@ -13,7 +16,11 @@ import org.junit.runner.RunWith class RealDeviceE2ETests { @get:Rule - val composeTestRule = createAndroidComposeRule() + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + // Workaround to find Compose UI elements on Espresso tests + @get:Rule + val composeTestRule: ComposeTestRule = createEmptyComposeRule() @Test fun testVaultLockUnlockFlow() { From 2af019e555092a5c1f74e53837a1dea573cf6cb3 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Thu, 26 Jun 2025 15:15:53 -0300 Subject: [PATCH 05/37] Updating code to read credentials from an external json file --- .sauce/config.yml | 32 +++++++++++++++++++ app/build.gradle.kts | 1 + app/src/androidTest/kotlin/data/TestData.kt | 7 ++++ .../androidTest/kotlin/data/TestDataReader.kt | 16 ++++++++++ .../accountSecurity/AccountSecurityPage.kt | 1 + .../e2e/pageObjects/vault/UnlockVaultPage.kt | 2 ++ .../kotlin/e2e/tests/BaseE2ETest.kt | 20 ++++++++++++ .../kotlin/e2e/tests/RealDeviceE2ETests.kt | 21 +++--------- 8 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 .sauce/config.yml create mode 100644 app/src/androidTest/kotlin/data/TestData.kt create mode 100644 app/src/androidTest/kotlin/data/TestDataReader.kt create mode 100644 app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt diff --git a/.sauce/config.yml b/.sauce/config.yml new file mode 100644 index 00000000000..46edea42659 --- /dev/null +++ b/.sauce/config.yml @@ -0,0 +1,32 @@ +apiVersion: v1alpha +kind: espresso +defaults: + timeout: 10m +sauce: + region: us-west-1 + # Controls how many suites are executed at the same time (sauce test env only). + concurrency: 1 + retries: 1 + visibility: team + metadata: + tags: + - Android + - sanity-e2e + build: Sanity check on Real devices +reporters: + junit: + enabled: true + filename: saucectl-report.xml +espresso: + app: storage:filename=com.x8bit.bitwarden.apk + testApp: storage:filename=com.x8bit.bitwarden-release-debug-androidTest.apk +suites: + - name: "Android - Sanity" + devices: + - name: "Google.*" + platformVersion: "^1[3456].*" + options: + deviceType: PHONE + testOptions: + package: e2e.tests + resigningEnabled: false diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6f5896994f..de9446cca1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -301,6 +301,7 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4.android) androidTestImplementation("androidx.test:runner:1.6.0") androidTestImplementation("androidx.test:rules:1.6.0") + androidTestImplementation("com.google.code.gson:gson:2.8.9") } tasks { diff --git a/app/src/androidTest/kotlin/data/TestData.kt b/app/src/androidTest/kotlin/data/TestData.kt new file mode 100644 index 00000000000..58ee99495e5 --- /dev/null +++ b/app/src/androidTest/kotlin/data/TestData.kt @@ -0,0 +1,7 @@ +package data + +data class TestData( + val baseUrl: String, + val email: String, + val password: String +) diff --git a/app/src/androidTest/kotlin/data/TestDataReader.kt b/app/src/androidTest/kotlin/data/TestDataReader.kt new file mode 100644 index 00000000000..79731da32f9 --- /dev/null +++ b/app/src/androidTest/kotlin/data/TestDataReader.kt @@ -0,0 +1,16 @@ +package data + +import androidx.test.platform.app.InstrumentationRegistry +import com.google.gson.Gson +import java.io.InputStreamReader +import java.nio.file.Paths + +object TestDataReader { + fun getTestData(fileName: String): TestData { + val context = InstrumentationRegistry.getInstrumentation().context.assets; + val inputStream = context.open(fileName) + val reader = InputStreamReader(inputStream) + return Gson().fromJson(reader, TestData::class.java) + + } +} diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt index 2b468b5d123..e29c6fa6ce2 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt @@ -20,6 +20,7 @@ class AccountSecurityPage(composeTestRule: ComposeTestRule) : Page(composeTestRu */ fun lockVault(): UnlockVaultPage { lockNowLabel.performScrollTo().performClick() + lockNowLabel.assertIsNotDisplayed() return UnlockVaultPage(composeTestRule) } } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt index 8b65db3cd08..1191aa43408 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt @@ -1,5 +1,6 @@ package e2e.pageObjects.vault +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -19,6 +20,7 @@ class UnlockVaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) } fun performUnlockVault(password: String): VaultPage { + unlockVaultButtonTag.assertIsDisplayed() passwordEntryTag.performClick().performTextInput(password) unlockVaultButtonTag.performClick() return VaultPage(composeTestRule) diff --git a/app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt b/app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt new file mode 100644 index 00000000000..3f5e6cb9e8b --- /dev/null +++ b/app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt @@ -0,0 +1,20 @@ +package e2e.tests + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.x8bit.bitwarden.MainActivity +import data.TestDataReader +import org.junit.Rule + +open class BaseE2ETest { + + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + // Workaround to find Compose UI elements on Espresso tests + @get:Rule + val composeTestRule: ComposeTestRule = createEmptyComposeRule() + + val testData = TestDataReader.getTestData("TestData.json") +} diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt index 5a728cc3a67..23ec3177180 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt @@ -1,38 +1,25 @@ package e2e.tests import androidx.compose.ui.test.* -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.junit4.createEmptyComposeRule -import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.x8bit.bitwarden.MainActivity import e2e.pageObjects.login.MainPage -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class RealDeviceE2ETests { - - @get:Rule - val activityRule = ActivityScenarioRule(MainActivity::class.java) - - // Workaround to find Compose UI elements on Espresso tests - @get:Rule - val composeTestRule: ComposeTestRule = createEmptyComposeRule() +class RealDeviceE2ETests : BaseE2ETest() { @Test fun testVaultLockUnlockFlow() { MainPage(composeTestRule) .startLogin() .openEnvironmentSettings() - .setupEnvironment("-") - .performLogin("-", "-") + .setupEnvironment(testData.baseUrl) + .performLogin(testData.email, testData.password) .navigateToSettingsPage() .navigateToAccountSecurity() .lockVault() - .performUnlockVault("-") + .performUnlockVault(testData.password) .assertVaultIsUnlocked() } } From 6181378f2844a5dd95a3ee51c696ce69521b91cd Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 27 Jun 2025 10:26:44 -0300 Subject: [PATCH 06/37] Adding missing steps to test-device workflow --- .github/workflows/test-device.yml | 110 ++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index f81f00ff407..029606fa99a 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -7,10 +7,112 @@ permissions: contents: read jobs: - test: - name: Test Device + test-device: + name: Check main build against real devices runs-on: ubuntu-24.04 + env: + JAVA_VERSION: 17 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} steps: - - name: Placeholder step - run: echo "Placeholder workflow step" + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Cache Gradle files + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }} + restore-keys: | + ${{ runner.os }}-gradle-v2- + + - name: Cache build output + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ github.workspace }}/build-cache + key: ${{ runner.os }}-build-cache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Configure JDK + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Configure Ruby + uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0 + with: + bundler-cache: true + + - name: Install Fastlane + run: | + gem install bundler:2.2.27 + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + + - name: Retrieve test data + uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 + with: + access_token: ${{ secrets.BWS_ACCESS_TOKEN }} + secrets: | + e4b23903-31d8-4989-9193-b30900d543f0 > TEST_ACCOUNT_CREDS + + - name: Configure .json test data file + run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json + + - name: Build release APK + env: + PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} + run: | + bundle exec fastlane assemblePlayStoreReleaseApk \ + storeFile:app_play-keystore.jks \ + storePassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' \ + keyAlias:bitwarden \ + keyPassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' + + - name: Build test APK (espresso) + run: | + ./gradlew :app:assembleStandardReleaseAndroidTest + + - name: Install saucectl + run: | + curl -L https://github.com/saucelabs/saucectl/releases/latest/download/saucectl_linux_amd64.tar.gz | tar -xz + sudo mv saucectl /usr/local/bin/ + + - name: Upload app APK to SauceLabs storage + run: | + saucectl storage upload --app com.x8bit.bitwarden.apk + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + + - name: Upload test APK to SauceLabs storage + run: | + saucectl storage upload --app com.x8bit.bitwarden-androidTest.apk + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + + - name: Run tests on SauceLabs + run: saucectl run --config .sauce/config.yml + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + + - name: Upload SauceLabs test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: saucectl-report + path: saucectl-report.xml + + + From a2ed70662548fbfd580a0ec198285b5b1a8ab41e Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 27 Jun 2025 14:34:07 -0300 Subject: [PATCH 07/37] Adding missing steps to retrieve secrets from Azure --- .github/workflows/test-device.yml | 22 ++++++++++++++++++++-- .sauce/config.yml | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 029606fa99a..6bba649bd75 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -68,6 +68,24 @@ jobs: - name: Configure .json test data file run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json + - name: Log in to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + env: + ACCOUNT_NAME: bitwardenci + CONTAINER_NAME: mobile + run: | + mkdir -p ${{ github.workspace }}/secrets + mkdir -p ${{ github.workspace }}/app/src/standardRelease + + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name app_play-keystore.jks --file ${{ github.workspace }}/keystores/app_play-keystore.jks --output none + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none + - name: Build release APK env: PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} @@ -89,14 +107,14 @@ jobs: - name: Upload app APK to SauceLabs storage run: | - saucectl storage upload --app com.x8bit.bitwarden.apk + saucectl storage upload --app app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - name: Upload test APK to SauceLabs storage run: | - saucectl storage upload --app com.x8bit.bitwarden-androidTest.apk + saucectl storage upload --app app/build/outputs/apk/standard/release/com.x8bit.bitwarden-androidTest.apk env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} diff --git a/.sauce/config.yml b/.sauce/config.yml index 46edea42659..353cdb2e329 100644 --- a/.sauce/config.yml +++ b/.sauce/config.yml @@ -19,7 +19,7 @@ reporters: filename: saucectl-report.xml espresso: app: storage:filename=com.x8bit.bitwarden.apk - testApp: storage:filename=com.x8bit.bitwarden-release-debug-androidTest.apk + testApp: storage:filename=com.x8bit.bitwarden-androidTest.apk suites: - name: "Android - Sanity" devices: From 285654588895408333263abf747e52f195c4683b Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 30 Jun 2025 16:56:32 -0300 Subject: [PATCH 08/37] Enabling release test build type --- .github/workflows/test-device.yml | 2 +- app/build.gradle.kts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 6bba649bd75..52a14b7ba86 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -114,7 +114,7 @@ jobs: - name: Upload test APK to SauceLabs storage run: | - saucectl storage upload --app app/build/outputs/apk/standard/release/com.x8bit.bitwarden-androidTest.apk + saucectl storage upload --app app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de9446cca1f..dd9937020f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,6 +47,8 @@ android { namespace = "com.x8bit.bitwarden" compileSdk = libs.versions.compileSdk.get().toInt() + testBuildType = "release" + room { schemaDirectory("$projectDir/schemas") } From a44a8dc6d4984a553814b900c1547872f9d323b2 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Tue, 1 Jul 2025 11:26:17 -0300 Subject: [PATCH 09/37] Pulling SM creds from Azure --- .github/workflows/test-device.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 52a14b7ba86..881d3603fe4 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -58,16 +58,6 @@ jobs: bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - - name: Retrieve test data - uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 - with: - access_token: ${{ secrets.BWS_ACCESS_TOKEN }} - secrets: | - e4b23903-31d8-4989-9193-b30900d543f0 > TEST_ACCOUNT_CREDS - - - name: Configure .json test data file - run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json - - name: Log in to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: @@ -86,6 +76,23 @@ jobs: az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none + - name: Get E2E secrets from Azure + id: get-e2e-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-test + secrets: "BWS-ACCESS-TOKEN" + + - name: Retrieve test data + uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 + with: + access_token: ${{ steps.get-e2e-secrets.outputs.BWS-ACCESS-TOKEN }} + secrets: | + e4b23903-31d8-4989-9193-b30900d543f0 > TEST_ACCOUNT_CREDS + + - name: Configure .json test data file + run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json + - name: Build release APK env: PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} From b0fb07110d7a6ee510d7bcaa5eb4bdf0ba82ad89 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Tue, 1 Jul 2025 11:45:30 -0300 Subject: [PATCH 10/37] Updating Azure creds to pull SM secrets --- .github/workflows/test-device.yml | 32 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 881d3603fe4..905855cf9d8 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -5,6 +5,9 @@ on: permissions: contents: read + actions: read + checks: write + id-token: write jobs: test-device: @@ -76,12 +79,22 @@ jobs: az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + - name: Get E2E secrets from Azure - id: get-e2e-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: gh-test + keyvault: gh-android secrets: "BWS-ACCESS-TOKEN" + id: get-e2e-secrets + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main - name: Retrieve test data uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 @@ -116,21 +129,21 @@ jobs: run: | saucectl storage upload --app app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} - name: Upload test APK to SauceLabs storage run: | saucectl storage upload --app app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} - name: Upload SauceLabs test report if: always() @@ -138,6 +151,3 @@ jobs: with: name: saucectl-report path: saucectl-report.xml - - - From a371efbd6be6829e3900b4051821cce0dec391b1 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Thu, 10 Jul 2025 19:51:48 -0300 Subject: [PATCH 11/37] Adding testData file --- app/src/androidTest/assets/TestData.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/src/androidTest/assets/TestData.json diff --git a/app/src/androidTest/assets/TestData.json b/app/src/androidTest/assets/TestData.json new file mode 100644 index 00000000000..cafde484b9e --- /dev/null +++ b/app/src/androidTest/assets/TestData.json @@ -0,0 +1,5 @@ +{ + "baseUrl": "_", + "email": "_", + "password": "_" +} From b8172f9c4b98e838229def3a899167d584017d4c Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Thu, 10 Jul 2025 20:24:22 -0300 Subject: [PATCH 12/37] Installing saucectl via npm --- .github/workflows/test-device.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 69a86a9fd86..ba5318113b0 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -61,6 +61,10 @@ jobs: bundle config path vendor/bundle bundle install --jobs 4 --retry 3 + - name: Install saucectl + run: | + npm i -g saucectl + - name: Log in to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: @@ -120,14 +124,9 @@ jobs: run: | ./gradlew :app:assembleStandardReleaseAndroidTest - - name: Install saucectl - run: | - curl -L https://github.com/saucelabs/saucectl/releases/latest/download/saucectl_linux_amd64.tar.gz | tar -xz - sudo mv saucectl /usr/local/bin/ - - name: Upload app APK to SauceLabs storage run: | - saucectl storage upload --app app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk + saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk env: SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} From 4b2f0da0aefcafb77ec96d4ce1ab4a4722baa774 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 08:37:47 -0300 Subject: [PATCH 13/37] Pulling SauceLabs creds from GH secrets --- .github/workflows/test-device.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index ba5318113b0..06f225a3eef 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -128,15 +128,15 @@ jobs: run: | saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - name: Upload test APK to SauceLabs storage run: | saucectl storage upload --app app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml From 6b2ac2965589d9a34f38e27710d8b3ee2a932265 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 10:08:09 -0300 Subject: [PATCH 14/37] Revert "Pulling SauceLabs creds from GH secrets" This reverts commit 4b2f0da0aefcafb77ec96d4ce1ab4a4722baa774. --- .github/workflows/test-device.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 06f225a3eef..ba5318113b0 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -128,15 +128,15 @@ jobs: run: | saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} - name: Upload test APK to SauceLabs storage run: | saucectl storage upload --app app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk env: - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml From 22644ab21c50e59507796b55dc3cca0dfe6eef2f Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 10:25:40 -0300 Subject: [PATCH 15/37] Fixing SauceLabs creds retrieval --- .github/workflows/test-device.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index ba5318113b0..adeb99efcc7 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -16,8 +16,6 @@ jobs: env: JAVA_VERSION: 17 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} steps: - name: Check out repo @@ -94,7 +92,7 @@ jobs: uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: gh-android - secrets: "BWS-ACCESS-TOKEN" + secrets: "BWS-ACCESS-TOKEN, SAUCE-LABS-USERNAME, SAUCE-LABS-ACCESS-KEY" id: get-e2e-secrets - name: Log out from Azure @@ -128,21 +126,21 @@ jobs: run: | saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Upload test APK to SauceLabs storage run: | - saucectl storage upload --app app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + saucectl storage upload app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE_USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE_ACCESS_KEY }} + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Upload SauceLabs test report if: always() From 5fc3c0227ed3247fd8777a1c8cff6b7bcbe15c9f Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 12:03:52 -0300 Subject: [PATCH 16/37] Fixing testApp filename --- .sauce/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sauce/config.yml b/.sauce/config.yml index 353cdb2e329..ac04c40d464 100644 --- a/.sauce/config.yml +++ b/.sauce/config.yml @@ -19,7 +19,7 @@ reporters: filename: saucectl-report.xml espresso: app: storage:filename=com.x8bit.bitwarden.apk - testApp: storage:filename=com.x8bit.bitwarden-androidTest.apk + testApp: storage:filename=com.x8bit.bitwarden-standard-release-androidTest.apk suites: - name: "Android - Sanity" devices: From e9afd929120eb9e06322bf441134d4174005476a Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 13:42:35 -0300 Subject: [PATCH 17/37] Signing testApp --- .github/workflows/test-device.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index adeb99efcc7..9e609009193 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -120,7 +120,11 @@ jobs: - name: Build test APK (espresso) run: | - ./gradlew :app:assembleStandardReleaseAndroidTest + bundle exec fastlane assembleStandardReleaseAndroidTest \ + storeFile:app_play-keystore.jks \ + storePassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' \ + keyAlias:bitwarden \ + keyPassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' - name: Upload app APK to SauceLabs storage run: | From a6bd9b6dda8ff8c1d004387de31dd252663a6a9c Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 15:34:17 -0300 Subject: [PATCH 18/37] Signing testApp --- .github/workflows/test-device.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index 9e609009193..cde0434c7aa 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -120,11 +120,16 @@ jobs: - name: Build test APK (espresso) run: | - bundle exec fastlane assembleStandardReleaseAndroidTest \ - storeFile:app_play-keystore.jks \ - storePassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' \ - keyAlias:bitwarden \ - keyPassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' + ./gradlew :app:assembleStandardReleaseAndroidTest + + - name: Signing test APK + run: | + $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ + --ks keystores/app_play-keystore.jks \ + --ks-key-alias bitwarden \ + --ks-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ + --key-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ + app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk - name: Upload app APK to SauceLabs storage run: | From c6f8fe243128cdbc43c7bf4c571abada9ee62fa8 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 15:34:17 -0300 Subject: [PATCH 19/37] Signing testApp --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd9937020f5..3c3e624cbf0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -301,6 +301,7 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit.ktx) androidTestImplementation(libs.androidx.ui.test.junit4.android) + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.0") androidTestImplementation("androidx.test:runner:1.6.0") androidTestImplementation("androidx.test:rules:1.6.0") androidTestImplementation("com.google.code.gson:gson:2.8.9") From 9a724d805b534894b652849f2ca3927b6260c1d2 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 18:05:25 -0300 Subject: [PATCH 20/37] Fixing importing issues during test execution --- app/build.gradle.kts | 7 ++----- app/proguard-rules.pro | 7 +++++++ gradle/libs.versions.toml | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c3e624cbf0..a10eff102f7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -286,7 +286,6 @@ dependencies { testImplementation(testFixtures(project(":network"))) testImplementation(testFixtures(project(":ui"))) - testImplementation(libs.androidx.compose.ui.test) testImplementation(libs.google.hilt.android.testing) testImplementation(platform(libs.junit.bom)) testRuntimeOnly(libs.junit.platform.launcher) @@ -301,10 +300,8 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit.ktx) androidTestImplementation(libs.androidx.ui.test.junit4.android) - androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.0") - androidTestImplementation("androidx.test:runner:1.6.0") - androidTestImplementation("androidx.test:rules:1.6.0") - androidTestImplementation("com.google.code.gson:gson:2.8.9") + androidTestImplementation(libs.androidx.compose.ui.test) + androidTestImplementation(libs.google.gson) } tasks { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b291901c0c7..2861aa8df25 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -132,3 +132,10 @@ -keep class androidx.test.ext.junit.** { *; } -keep class androidx.test.ext.** { *; } -keep class androidx.test.** { *; } + +# Keep Compose test classes +-keep class androidx.compose.ui.test.** { *; } +-keep class androidx.compose.ui.test.junit4.** { *; } + +# Keep Gson classes +-keep class com.google.gson.** { *; } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e28a62874c3..cb28871d2bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,7 @@ detekt = "1.23.8" firebaseBom = "33.16.0" glide = "1.0.0-beta01" googleGuava = "33.4.8-jre" +googleGson = "2.10.1" googleProtoBufJava = "4.31.1" googleProtoBufPlugin = "0.9.5" googleServices = "4.4.3" @@ -101,6 +102,7 @@ detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libra google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } google-firebase-cloud-messaging = { module = "com.google.firebase:firebase-messaging" } google-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } +google-gson = { module = "com.google.code.gson:gson", version.ref = "googleGson" } google-guava = { module = "com.google.guava:guava", version.ref = "googleGuava" } google-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } google-hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } From a5721f3e5912377dd232e70c9fc3e5fc131f75eb Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 11 Jul 2025 18:44:09 -0300 Subject: [PATCH 21/37] Updating the way we read json data --- app/build.gradle.kts | 1 - app/proguard-rules.pro | 6 ++++-- app/src/androidTest/kotlin/data/TestData.kt | 3 +++ .../androidTest/kotlin/data/TestDataReader.kt | 19 +++++++++++-------- gradle/libs.versions.toml | 2 -- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a10eff102f7..13435f8dc4f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -301,7 +301,6 @@ dependencies { androidTestImplementation(libs.androidx.junit.ktx) androidTestImplementation(libs.androidx.ui.test.junit4.android) androidTestImplementation(libs.androidx.compose.ui.test) - androidTestImplementation(libs.google.gson) } tasks { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2861aa8df25..7e71c02a196 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -137,5 +137,7 @@ -keep class androidx.compose.ui.test.** { *; } -keep class androidx.compose.ui.test.junit4.** { *; } -# Keep Gson classes --keep class com.google.gson.** { *; } +# Keep Kotlin standard library classes +-keep class kotlin.** { *; } +-keep class kotlinx.** { *; } +-keep class kotlin.io.** { *; } diff --git a/app/src/androidTest/kotlin/data/TestData.kt b/app/src/androidTest/kotlin/data/TestData.kt index 58ee99495e5..406ffdb6077 100644 --- a/app/src/androidTest/kotlin/data/TestData.kt +++ b/app/src/androidTest/kotlin/data/TestData.kt @@ -1,5 +1,8 @@ package data +import kotlinx.serialization.Serializable + +@Serializable data class TestData( val baseUrl: String, val email: String, diff --git a/app/src/androidTest/kotlin/data/TestDataReader.kt b/app/src/androidTest/kotlin/data/TestDataReader.kt index 79731da32f9..0dcd25093dd 100644 --- a/app/src/androidTest/kotlin/data/TestDataReader.kt +++ b/app/src/androidTest/kotlin/data/TestDataReader.kt @@ -1,16 +1,19 @@ package data import androidx.test.platform.app.InstrumentationRegistry -import com.google.gson.Gson -import java.io.InputStreamReader -import java.nio.file.Paths +import kotlinx.serialization.json.Json +import java.io.InputStream +import java.nio.charset.StandardCharsets object TestDataReader { fun getTestData(fileName: String): TestData { - val context = InstrumentationRegistry.getInstrumentation().context.assets; - val inputStream = context.open(fileName) - val reader = InputStreamReader(inputStream) - return Gson().fromJson(reader, TestData::class.java) - + val context = InstrumentationRegistry.getInstrumentation().context.assets + val inputStream: InputStream = context.open(fileName) + val size = inputStream.available() + val buffer = ByteArray(size) + inputStream.read(buffer) + inputStream.close() + val jsonString = String(buffer, StandardCharsets.UTF_8) + return Json.decodeFromString(jsonString) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb28871d2bf..e28a62874c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,6 @@ detekt = "1.23.8" firebaseBom = "33.16.0" glide = "1.0.0-beta01" googleGuava = "33.4.8-jre" -googleGson = "2.10.1" googleProtoBufJava = "4.31.1" googleProtoBufPlugin = "0.9.5" googleServices = "4.4.3" @@ -102,7 +101,6 @@ detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libra google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } google-firebase-cloud-messaging = { module = "com.google.firebase:firebase-messaging" } google-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } -google-gson = { module = "com.google.code.gson:gson", version.ref = "googleGson" } google-guava = { module = "com.google.guava:guava", version.ref = "googleGuava" } google-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } google-hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } From 7bedb1f4fd548c3a385731baf1895342c078e5a2 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 15:10:42 -0300 Subject: [PATCH 22/37] Adding steps to enable screen recording --- .../kotlin/e2e/pageObjects/login/LoginPage.kt | 14 ++++++++++++++ .../kotlin/e2e/tests/RealDeviceE2ETests.kt | 1 + .../ui/auth/feature/landing/LandingScreen.kt | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt index d4f5256c4ba..1fd8cbe100d 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt @@ -19,6 +19,11 @@ class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ private val continueButton by lazy { getElement("ContinueButton") } private val loginWithMasterPasswordButton by lazy { getElement("LogInWithMasterPasswordButton") } private val regionSelectorButton by lazy { getElement("RegionSelectorDropdown") } + private val openSettingsButton by lazy { getElement("AppSettingsButton") } + private val otherSettingsButton by lazy { getElement("OtherSettingsButton") } + private val allowScreenCaptureToggle by lazy { getElement("AllowScreenCaptureSwitch") } + private val goBackButton by lazy { getElement("CloseButton") } + /** * Enters the master password in the password field @@ -44,4 +49,13 @@ class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ .performClick() return EnvironmentSettingsPage(composeTestRule) } + + fun turnOnScreenRecording() : LoginPage { + openSettingsButton.performClick() + otherSettingsButton.performClick() + allowScreenCaptureToggle.performClick() + goBackButton.performClick() + goBackButton.performClick() + return this + } } diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt index 23ec3177180..f9dddeff1cc 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt @@ -13,6 +13,7 @@ class RealDeviceE2ETests : BaseE2ETest() { fun testVaultLockUnlockFlow() { MainPage(composeTestRule) .startLogin() + .turnOnScreenRecording() .openEnvironmentSettings() .setupEnvironment(testData.baseUrl) .performLogin(testData.email, testData.password) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index f7a84a69374..d9e0b0d5f0d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -326,7 +326,8 @@ private fun LandingScreenContent( icon = rememberVectorPainter(id = BitwardenDrawable.ic_cog), modifier = Modifier .standardHorizontalMargin() - .fillMaxWidth(), + .fillMaxWidth() + .testTag("AppSettingsButton"), ) Spacer(modifier = Modifier.height(height = 12.dp)) From 85f0bd920ce3ed4d8f55fe7b2819e5df52b2f58a Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 15:41:09 -0300 Subject: [PATCH 23/37] Removing steps to test faster on Device Farm --- .github/workflows/test-device.yml | 39 ------------------- .../kotlin/e2e/tests/RealDeviceE2ETests.kt | 5 ++- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index cde0434c7aa..fd468984732 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -105,45 +105,6 @@ jobs: secrets: | 63e93f73-5118-4a62-9db8-b3160176aa8a > TEST_ACCOUNT_CREDS - - name: Configure .json test data file - run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json - - - name: Build release APK - env: - PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} - run: | - bundle exec fastlane assemblePlayStoreReleaseApk \ - storeFile:app_play-keystore.jks \ - storePassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' \ - keyAlias:bitwarden \ - keyPassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' - - - name: Build test APK (espresso) - run: | - ./gradlew :app:assembleStandardReleaseAndroidTest - - - name: Signing test APK - run: | - $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ - --ks keystores/app_play-keystore.jks \ - --ks-key-alias bitwarden \ - --ks-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ - --key-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ - app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk - - - name: Upload app APK to SauceLabs storage - run: | - saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk - env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - - - name: Upload test APK to SauceLabs storage - run: | - saucectl storage upload app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk - env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt index f9dddeff1cc..33ac4824d52 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt @@ -11,13 +11,14 @@ class RealDeviceE2ETests : BaseE2ETest() { @Test fun testVaultLockUnlockFlow() { - MainPage(composeTestRule) + var vault = MainPage(composeTestRule) .startLogin() .turnOnScreenRecording() .openEnvironmentSettings() .setupEnvironment(testData.baseUrl) .performLogin(testData.email, testData.password) - .navigateToSettingsPage() + vault.assertVaultIsUnlocked() + vault.navigateToSettingsPage() .navigateToAccountSecurity() .lockVault() .performUnlockVault(testData.password) From d3d02f4a10a5d38cb138d60ad6ceab7f7c0642a4 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 15:52:08 -0300 Subject: [PATCH 24/37] Increasing timeout --- app/src/androidTest/kotlin/e2e/pageObjects/Page.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt index 53d43f0f9c3..41d0879ce2d 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.test.performClick */ abstract class Page(protected val composeTestRule: ComposeTestRule) { companion object { - val TIMEOUT_MILLIS = 30000L + val TIMEOUT_MILLIS = 60000L } /** From cab4461bcf8d7c900c59f6190cb0318a74cad97c Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 16:10:16 -0300 Subject: [PATCH 25/37] Decreasing timeout --- app/src/androidTest/kotlin/e2e/pageObjects/Page.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt index 41d0879ce2d..53d43f0f9c3 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.test.performClick */ abstract class Page(protected val composeTestRule: ComposeTestRule) { companion object { - val TIMEOUT_MILLIS = 60000L + val TIMEOUT_MILLIS = 30000L } /** From 94741336227741c4473c3f6d52d582b8b8e42011 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 16:30:34 -0300 Subject: [PATCH 26/37] Restoring build steps --- .github/workflows/test-device.yml | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index fd468984732..cde0434c7aa 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -105,6 +105,45 @@ jobs: secrets: | 63e93f73-5118-4a62-9db8-b3160176aa8a > TEST_ACCOUNT_CREDS + - name: Configure .json test data file + run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json + + - name: Build release APK + env: + PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} + run: | + bundle exec fastlane assemblePlayStoreReleaseApk \ + storeFile:app_play-keystore.jks \ + storePassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' \ + keyAlias:bitwarden \ + keyPassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' + + - name: Build test APK (espresso) + run: | + ./gradlew :app:assembleStandardReleaseAndroidTest + + - name: Signing test APK + run: | + $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ + --ks keystores/app_play-keystore.jks \ + --ks-key-alias bitwarden \ + --ks-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ + --key-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ + app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + + - name: Upload app APK to SauceLabs storage + run: | + saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk + env: + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + + - name: Upload test APK to SauceLabs storage + run: | + saucectl storage upload app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + env: + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml From 7dc5b99342d392c60bc88bad27cebadd94c88ee3 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 16:38:33 -0300 Subject: [PATCH 27/37] Fix formatting --- app/src/androidTest/kotlin/data/TestData.kt | 2 +- app/src/androidTest/kotlin/e2e/pageObjects/Page.kt | 4 ++-- .../e2e/pageObjects/login/EnvironmentSettingsPage.kt | 4 ++-- .../androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt | 7 +++---- .../kotlin/e2e/pageObjects/settings/SettingsPage.kt | 2 +- .../settings/accountSecurity/AccountSecurityPage.kt | 2 +- .../kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt | 1 - 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/src/androidTest/kotlin/data/TestData.kt b/app/src/androidTest/kotlin/data/TestData.kt index 406ffdb6077..7ee6816f604 100644 --- a/app/src/androidTest/kotlin/data/TestData.kt +++ b/app/src/androidTest/kotlin/data/TestData.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable data class TestData( val baseUrl: String, val email: String, - val password: String + val password: String, ) diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt index 53d43f0f9c3..704696ff7cb 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -38,7 +38,7 @@ abstract class Page(protected val composeTestRule: ComposeTestRule) { return composeTestRule.onNodeWithTag(testTag) } - protected fun getElementByText(text: String): SemanticsNodeInteraction{ + protected fun getElementByText(text: String): SemanticsNodeInteraction { waitForIdle() waitUntil(TIMEOUT_MILLIS) { try { @@ -66,7 +66,7 @@ abstract class Page(protected val composeTestRule: ComposeTestRule) { */ protected fun waitUntil( timeoutMillis: Long, - condition: () -> Boolean + condition: () -> Boolean, ) { composeTestRule.waitUntil(timeoutMillis) { condition() } } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt index 556f1c3713d..bb264f412da 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt @@ -6,12 +6,12 @@ import androidx.compose.ui.test.performTextInput import com.x8bit.bitwarden.e2e.pages.LoginPage import e2e.pageObjects.Page -class EnvironmentSettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ +class EnvironmentSettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { private val serverURLField by lazy { getElement("ServerUrlEntry") } private val saveButton by lazy { getElement("SaveButton") } - fun setupEnvironment(url: String) : LoginPage { + fun setupEnvironment(url: String): LoginPage { serverURLField .performClick() .performTextInput(url) diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt index 1fd8cbe100d..ac09489dc7e 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt @@ -11,7 +11,7 @@ import e2e.pageObjects.vault.VaultPage * Page Object representing the Login screen of the Bitwarden app. * This class encapsulates all the UI elements and actions available on the login screen. */ -class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ +class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { // UI Elements private val emailField by lazy { getElement("EmailAddressEntry") } @@ -24,7 +24,6 @@ class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ private val allowScreenCaptureToggle by lazy { getElement("AllowScreenCaptureSwitch") } private val goBackButton by lazy { getElement("CloseButton") } - /** * Enters the master password in the password field * @param password The master password to enter @@ -43,14 +42,14 @@ class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule){ return VaultPage(composeTestRule) } - fun openEnvironmentSettings() : EnvironmentSettingsPage { + fun openEnvironmentSettings(): EnvironmentSettingsPage { regionSelectorButton.performClick() getElementByText("Self-hosted") .performClick() return EnvironmentSettingsPage(composeTestRule) } - fun turnOnScreenRecording() : LoginPage { + fun turnOnScreenRecording(): LoginPage { openSettingsButton.performClick() otherSettingsButton.performClick() allowScreenCaptureToggle.performClick() diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt index 50a5312fc02..e72123a8a49 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.test.* import e2e.pageObjects.Page import e2e.pageObjects.settings.accountSecurity.AccountSecurityPage -class SettingsPage (composeTestRule: ComposeTestRule) : Page(composeTestRule) { +class SettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { // UI Elements private val accountSecurityButton by lazy { getElement("AccountSecuritySettingsButton") } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt index e29c6fa6ce2..a1882e06c05 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt @@ -9,7 +9,7 @@ import e2e.pageObjects.vault.UnlockVaultPage * Page Object representing the Account Security screen of the Bitwarden app. * This class encapsulates all the UI elements and actions available on the account security screen. */ -class AccountSecurityPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { +class AccountSecurityPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { // UI Elements private val lockNowLabel by lazy { getElement("LockNowLabel") } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt index 1191aa43408..5ae610a3f5d 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt @@ -13,7 +13,6 @@ class UnlockVaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) private val passwordEntryTag by lazy { getElement("MasterPasswordEntry") } private val unlockVaultButtonTag by lazy { getElement("UnlockVaultButton") } - fun enterPassword(password: String): UnlockVaultPage { passwordEntryTag.performTextInput(password) return this From 909b0d03cb20c580ae239030701b4242e61668f0 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 14 Jul 2025 16:59:07 -0300 Subject: [PATCH 28/37] Integrating e2e test run unto build workflow --- .github/workflows/build.yml | 150 +++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 432b965be6f..4a5c033bdd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,8 +112,8 @@ jobs: strategy: fail-fast: false matrix: - variant: ["prod", "dev"] - artifact: ["apk", "aab"] + variant: [ "prod", "dev" ] + artifact: [ "apk", "aab" ] steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -577,3 +577,149 @@ jobs: bundle exec fastlane distributeReleaseFDroidToFirebase \ actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \ service_credentials_file:${{ env.APP_FDROID_FIREBASE_CREDS_PATH }} + + test_on_device: + name: Running sanity check on a real device + needs: publish_playstore + runs-on: ubuntu-24.04 + if: github.ref == 'refs/heads/main' + env: + JAVA_VERSION: 17 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Cache Gradle files + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }} + restore-keys: | + ${{ runner.os }}-gradle-v2- + + - name: Cache build output + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/build-cache + key: ${{ runner.os }}-build-cache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Configure JDK + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Configure Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Install Fastlane + run: | + gem install bundler:2.2.27 + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + + - name: Install saucectl + run: | + npm i -g saucectl + + - name: Log in to Azure + uses: Azure/login@v1 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + env: + ACCOUNT_NAME: bitwardenci + CONTAINER_NAME: mobile + run: | + mkdir -p ${{ github.workspace }}/secrets + mkdir -p ${{ github.workspace }}/app/src/standardRelease + + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name app_play-keystore.jks --file ${{ github.workspace }}/keystores/app_play-keystore.jks --output none + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none + + - name: Log in to Azure (Bitwarden) + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get E2E test ecrets from Azure + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-android + secrets: "BWS-ACCESS-TOKEN, SAUCE-LABS-USERNAME, SAUCE-LABS-ACCESS-KEY" + id: get-e2e-secrets + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Retrieve test data + uses: bitwarden/sm-action@v2.2.0 + with: + access_token: ${{ steps.get-e2e-secrets.outputs.BWS-ACCESS-TOKEN }} + secrets: | + 63e93f73-5118-4a62-9db8-b3160176aa8a > TEST_ACCOUNT_CREDS + + - name: Configure .json test data file + run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json + + - name: Build test APK (espresso) + run: | + ./gradlew :app:assembleStandardReleaseAndroidTest + + - name: Download release APK artifact + uses: actions/download-artifact@v4 + with: + name: com.x8bit.bitwarden.apk + path: app/build/outputs/apk/standard/release/ + + - name: Signing test APK + run: | + $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ + --ks keystores/app_play-keystore.jks \ + --ks-key-alias bitwarden \ + --ks-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ + --key-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ + app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + + - name: Upload APK to SauceLabs storage + run: | + saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk + env: + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + + - name: Upload test APK to SauceLabs storage + run: | + saucectl storage upload app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + env: + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + + - name: Run tests on SauceLabs + run: saucectl run --config .sauce/config.yml + env: + SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + + - name: Upload SauceLabs test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: saucectl-report + path: saucectl-report.xml From bbd7e7c00d85335a57695b22ee6776431e7e818c Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Tue, 15 Jul 2025 09:49:33 -0300 Subject: [PATCH 29/37] Adding suggestions --- .../e2e/tests/{BaseE2ETest.kt => BaseE2eTest.kt} | 2 +- ...alDeviceE2ETests.kt => RealDeviceE2eTests.kt} | 2 +- gradle/libs.versions.toml | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) rename app/src/androidTest/kotlin/e2e/tests/{BaseE2ETest.kt => BaseE2eTest.kt} (95%) rename app/src/androidTest/kotlin/e2e/tests/{RealDeviceE2ETests.kt => RealDeviceE2eTests.kt} (94%) diff --git a/app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt b/app/src/androidTest/kotlin/e2e/tests/BaseE2eTest.kt similarity index 95% rename from app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt rename to app/src/androidTest/kotlin/e2e/tests/BaseE2eTest.kt index 3f5e6cb9e8b..7a6e5a34606 100644 --- a/app/src/androidTest/kotlin/e2e/tests/BaseE2ETest.kt +++ b/app/src/androidTest/kotlin/e2e/tests/BaseE2eTest.kt @@ -7,7 +7,7 @@ import com.x8bit.bitwarden.MainActivity import data.TestDataReader import org.junit.Rule -open class BaseE2ETest { +open class BaseE2eTest { @get:Rule val activityRule = ActivityScenarioRule(MainActivity::class.java) diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt similarity index 94% rename from app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt rename to app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt index 33ac4824d52..fef12328da0 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2ETests.kt +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt @@ -7,7 +7,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class RealDeviceE2ETests : BaseE2ETest() { +class RealDeviceE2eTests : BaseE2eTest() { @Test fun testVaultLockUnlockFlow() { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e28a62874c3..ecf19a776af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ androidxWork = "2.10.2" bitwardenSdk = "1.0.0-20250708.105256-238" crashlytics = "3.0.4" detekt = "1.23.8" +espressoCore = "3.6.1" firebaseBom = "33.16.0" glide = "1.0.0-beta01" googleGuava = "33.4.8-jre" @@ -37,6 +38,7 @@ googleServices = "4.4.3" googleReview = "2.0.2" hilt = "2.56.2" junit5 = "5.13.2" +junitKtx = "1.2.1" jvmTarget = "17" # kotlin and ksp **must** use compatible versions, do not update either without the other. kotlin = "2.2.0" @@ -53,11 +55,8 @@ sonarqube = "6.2.0.5505" testng = "7.11.0" timber = "5.0.1" turbine = "1.2.1" -zxing = "3.5.3" uiautomator = "2.3.0" -espressoCore = "3.6.1" -junitKtx = "1.2.1" -uiTestJunit4Android = "1.8.2" +zxing = "3.5.3" [libraries] # Format: - @@ -81,9 +80,12 @@ androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-mani androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } + #noinspection CredentialDependency - Used for Passkey support, which is not available below Android 14 androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "androidxCredentials" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } @@ -93,6 +95,8 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" } androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidxSecurityCrypto" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" } +androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } +androidx-ui-test-junit4-android = { group = "androidx.compose.ui", name = "ui-test-junit4-android" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" } bitwarden-sdk = { module = "com.bitwarden:sdk-android-temp", version.ref = "bitwardenSdk" } bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } @@ -130,10 +134,6 @@ square-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } zxing-zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" } testng = { group = "org.testng", name = "testng", version.ref = "testng" } -androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" } -androidx-ui-test-junit4-android = { group = "androidx.compose.ui", name = "ui-test-junit4-android", version.ref = "uiTestJunit4Android" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From 62b9088a5a051755c94ff4d6f5a340c425f3e403 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Tue, 15 Jul 2025 12:45:55 -0300 Subject: [PATCH 30/37] Fixing lint issues --- .github/workflows/build.yml | 28 ++++++++++++++-------------- .github/workflows/test-device.yml | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a5c033bdd1..193681d0dce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -584,18 +584,18 @@ jobs: runs-on: ubuntu-24.04 if: github.ref == 'refs/heads/main' env: - JAVA_VERSION: 17 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + _JAVA_VERSION: 17 + _GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 - name: Cache Gradle files - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | ~/.gradle/caches @@ -605,7 +605,7 @@ jobs: ${{ runner.os }}-gradle-v2- - name: Cache build output - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ${{ github.workspace }}/build-cache key: ${{ runner.os }}-build-cache-${{ github.sha }} @@ -613,13 +613,13 @@ jobs: ${{ runner.os }}-build- - name: Configure JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: "temurin" - java-version: ${{ env.JAVA_VERSION }} + java-version: ${{ env._JAVA_VERSION }} - name: Configure Ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0 with: bundler-cache: true @@ -634,7 +634,7 @@ jobs: npm i -g saucectl - name: Log in to Azure - uses: Azure/login@v1 + uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -658,7 +658,7 @@ jobs: tenant_id: ${{ secrets.AZURE_TENANT_ID }} client_id: ${{ secrets.AZURE_CLIENT_ID }} - - name: Get E2E test ecrets from Azure + - name: Get E2E test secrets from Azure uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: gh-android @@ -669,7 +669,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Retrieve test data - uses: bitwarden/sm-action@v2.2.0 + uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 with: access_token: ${{ steps.get-e2e-secrets.outputs.BWS-ACCESS-TOKEN }} secrets: | @@ -683,7 +683,7 @@ jobs: ./gradlew :app:assembleStandardReleaseAndroidTest - name: Download release APK artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: com.x8bit.bitwarden.apk path: app/build/outputs/apk/standard/release/ @@ -719,7 +719,7 @@ jobs: - name: Upload SauceLabs test report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: saucectl-report path: saucectl-report.xml diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index cde0434c7aa..bacd2cdf08c 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -14,15 +14,15 @@ jobs: name: Check main build against real devices runs-on: ubuntu-24.04 env: - JAVA_VERSION: 17 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + _JAVA_VERSION: 17 + _GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 - name: Cache Gradle files uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 @@ -153,7 +153,7 @@ jobs: - name: Upload SauceLabs test report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: saucectl-report path: saucectl-report.xml From 7a985557aa8a650b079b6d00f6bc4522a2125e03 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 18 Jul 2025 11:09:40 -0300 Subject: [PATCH 31/37] fixing static analisys issues --- app/src/androidTest/kotlin/e2e/pageObjects/Page.kt | 4 ++-- .../androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt | 8 +++++--- .../androidTest/kotlin/e2e/pageObjects/login/MainPage.kt | 2 -- .../kotlin/e2e/pageObjects/settings/SettingsPage.kt | 2 +- .../settings/accountSecurity/AccountSecurityPage.kt | 4 +++- .../kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt | 2 -- .../androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt | 3 --- .../androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt | 1 - 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt index 704696ff7cb..b8beab66e05 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -16,11 +16,11 @@ import androidx.compose.ui.test.performClick */ abstract class Page(protected val composeTestRule: ComposeTestRule) { companion object { - val TIMEOUT_MILLIS = 30000L + const val TIMEOUT_MILLIS = 30000L } /** - * Waits for an element with the specified test tag to be present and returns its SemanticsNodeInteraction. + * Waits for an element with the specified test tag to be present. * @param testTag The test tag of the element to wait for * @return SemanticsNodeInteraction for the found element * @throws AssertionError if the element is not found within the timeout period diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt index ac09489dc7e..2a381bc29c4 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt @@ -1,8 +1,8 @@ package com.x8bit.bitwarden.e2e.pages -import androidx.compose.ui.test.* +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.junit4.ComposeTestRule -import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData import e2e.pageObjects.Page import e2e.pageObjects.login.EnvironmentSettingsPage import e2e.pageObjects.vault.VaultPage @@ -17,7 +17,9 @@ class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { private val emailField by lazy { getElement("EmailAddressEntry") } private val masterPasswordField by lazy { getElement("MasterPasswordEntry") } private val continueButton by lazy { getElement("ContinueButton") } - private val loginWithMasterPasswordButton by lazy { getElement("LogInWithMasterPasswordButton") } + private val loginWithMasterPasswordButton by lazy { + getElement("LogInWithMasterPasswordButton") + } private val regionSelectorButton by lazy { getElement("RegionSelectorDropdown") } private val openSettingsButton by lazy { getElement("AppSettingsButton") } private val otherSettingsButton by lazy { getElement("OtherSettingsButton") } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt index 9520a7dea29..b343107a11b 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt @@ -1,11 +1,9 @@ package e2e.pageObjects.login import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import com.x8bit.bitwarden.e2e.pages.LoginPage import e2e.pageObjects.Page -import e2e.pageObjects.vault.VaultPage class MainPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt index e72123a8a49..b57b3626e3c 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt @@ -1,7 +1,7 @@ package e2e.pageObjects.settings import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.* +import androidx.compose.ui.test.performClick import e2e.pageObjects.Page import e2e.pageObjects.settings.accountSecurity.AccountSecurityPage diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt index a1882e06c05..3b0ae3165a2 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt @@ -1,6 +1,8 @@ package e2e.pageObjects.settings.accountSecurity -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.junit4.ComposeTestRule import e2e.pageObjects.Page import e2e.pageObjects.vault.UnlockVaultPage diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt index 5ae610a3f5d..31df168d033 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt @@ -2,7 +2,6 @@ package e2e.pageObjects.vault import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput import e2e.pageObjects.Page @@ -24,5 +23,4 @@ class UnlockVaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) unlockVaultButtonTag.performClick() return VaultPage(composeTestRule) } - } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt index fd701ad9ce6..a146e4e4a41 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt @@ -1,11 +1,8 @@ package e2e.pageObjects.vault import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import e2e.pageObjects.Page import e2e.pageObjects.settings.SettingsPage diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt index fef12328da0..77c99a38cc7 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt +++ b/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt @@ -1,6 +1,5 @@ package e2e.tests -import androidx.compose.ui.test.* import androidx.test.ext.junit.runners.AndroidJUnit4 import e2e.pageObjects.login.MainPage import org.junit.Test From b1a74c6faed91520d9c4188d4d1bf9765ef984a6 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Tue, 22 Jul 2025 16:52:53 -0300 Subject: [PATCH 32/37] Adding suggestions --- .github/workflows/build.yml | 11 +++------- README.md | 21 +++++++++++++++++++ app/build.gradle.kts | 5 +++-- .../androidTest/kotlin/data/TestDataReader.kt | 14 ++++++------- .../kotlin/e2e/pageObjects/Page.kt | 6 +++--- .../login/EnvironmentSettingsPage.kt | 4 ++-- .../kotlin/e2e/pageObjects/login/LoginPage.kt | 1 - .../kotlin/e2e/pageObjects/login/MainPage.kt | 1 - .../e2e/pageObjects/settings/SettingsPage.kt | 1 - .../accountSecurity/AccountSecurityPage.kt | 1 - .../e2e/pageObjects/vault/UnlockVaultPage.kt | 1 - .../kotlin/e2e/pageObjects/vault/VaultPage.kt | 1 - 12 files changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a70fd679962..86db4076aa2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,8 +113,8 @@ jobs: strategy: fail-fast: false matrix: - variant: [ "prod", "dev" ] - artifact: [ "apk", "aab" ] + variant: ["prod", "dev"] + artifact: ["apk", "aab"] steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -658,11 +658,6 @@ jobs: run: | npm i -g saucectl - - name: Log in to Azure - uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - name: Retrieve secrets env: ACCOUNT_NAME: bitwardenci @@ -676,7 +671,7 @@ jobs: az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none - - name: Log in to Azure (Bitwarden) + - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main with: subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} diff --git a/README.md b/README.md index ba04f67b8ba..841322d178e 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,27 @@ The following is a list of additional third-party dependencies used as part of t - Purpose: A small testing library for kotlinx.coroutine's Flow. - License: Apache 2.0 +- **AndroidX Espresso Core** + - https://developer.android.com/jetpack/androidx/releases/espresso + - Purpose: UI testing framework for Android. + - License: Apache 2.0 + +- **AndroidX JUnit KTX** + - https://developer.android.com/jetpack/androidx/releases/junit + - Purpose: Kotlin extensions for JUnit-based Android tests. + - License: Apache 2.0 + +- **AndroidX UIAutomator** + - https://developer.android.com/training/testing/other-components/ui-automator + - Purpose: UI testing across multiple apps. + - License: Apache 2.0 + +- **AndroidX Compose UI Test JUnit4 (Android)** + - https://developer.android.com/jetpack/androidx/releases/compose-ui + - Purpose: Compose UI testing for Android using JUnit4. + - License: Apache 2.0 + + ### CI/CD Dependencies The following is a list of additional third-party dependencies used as part of the CI/CD workflows. These are not present in the final packaged application. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 77b1a37c54b..387b8fcb3fd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,6 +47,7 @@ android { namespace = "com.x8bit.bitwarden" compileSdk = libs.versions.compileSdk.get().toInt() + // Required for SauceLabs integration testBuildType = "release" room { @@ -299,11 +300,11 @@ dependencies { testImplementation(libs.mockk.mockk) testImplementation(libs.robolectric.robolectric) testImplementation(libs.square.turbine) - androidTestImplementation(libs.androidx.uiautomator) + androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit.ktx) androidTestImplementation(libs.androidx.ui.test.junit4.android) - androidTestImplementation(libs.androidx.compose.ui.test) + androidTestImplementation(libs.androidx.uiautomator) } tasks { diff --git a/app/src/androidTest/kotlin/data/TestDataReader.kt b/app/src/androidTest/kotlin/data/TestDataReader.kt index 0dcd25093dd..a09b62695f9 100644 --- a/app/src/androidTest/kotlin/data/TestDataReader.kt +++ b/app/src/androidTest/kotlin/data/TestDataReader.kt @@ -7,13 +7,13 @@ import java.nio.charset.StandardCharsets object TestDataReader { fun getTestData(fileName: String): TestData { - val context = InstrumentationRegistry.getInstrumentation().context.assets - val inputStream: InputStream = context.open(fileName) - val size = inputStream.available() - val buffer = ByteArray(size) - inputStream.read(buffer) - inputStream.close() - val jsonString = String(buffer, StandardCharsets.UTF_8) + val assets = InstrumentationRegistry.getInstrumentation().context.assets + val jsonString = assets + .open(fileName) + .use { inputStream -> + inputStream.bufferedReader(StandardCharsets.UTF_8) + .readText() + } return Json.decodeFromString(jsonString) } } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt index b8beab66e05..6d0d56d3517 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt @@ -27,7 +27,7 @@ abstract class Page(protected val composeTestRule: ComposeTestRule) { */ protected fun getElement(testTag: String): SemanticsNodeInteraction { waitForIdle() - waitUntil(TIMEOUT_MILLIS) { + waitUntil() { try { composeTestRule.onNodeWithTag(testTag).assertExists() true @@ -40,7 +40,7 @@ abstract class Page(protected val composeTestRule: ComposeTestRule) { protected fun getElementByText(text: String): SemanticsNodeInteraction { waitForIdle() - waitUntil(TIMEOUT_MILLIS) { + waitUntil() { try { composeTestRule.onNodeWithText(text).assertExists() true @@ -65,7 +65,7 @@ abstract class Page(protected val composeTestRule: ComposeTestRule) { * @param condition The condition to wait for */ protected fun waitUntil( - timeoutMillis: Long, + timeoutMillis: Long = TIMEOUT_MILLIS, condition: () -> Boolean, ) { composeTestRule.waitUntil(timeoutMillis) { condition() } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt index bb264f412da..a26db96d405 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt @@ -8,11 +8,11 @@ import e2e.pageObjects.Page class EnvironmentSettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - private val serverURLField by lazy { getElement("ServerUrlEntry") } + private val serverUrlField by lazy { getElement("ServerUrlEntry") } private val saveButton by lazy { getElement("SaveButton") } fun setupEnvironment(url: String): LoginPage { - serverURLField + serverUrlField .performClick() .performTextInput(url) saveButton.performClick() diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt index 2a381bc29c4..20d28939fa0 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt @@ -13,7 +13,6 @@ import e2e.pageObjects.vault.VaultPage */ class LoginPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - // UI Elements private val emailField by lazy { getElement("EmailAddressEntry") } private val masterPasswordField by lazy { getElement("MasterPasswordEntry") } private val continueButton by lazy { getElement("ContinueButton") } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt index b343107a11b..0ffac2102ba 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt @@ -7,7 +7,6 @@ import e2e.pageObjects.Page class MainPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - // UI Elements private val loginButton by lazy { getElement("ChooseLoginButton") } private val createAccountButton by lazy { getElement("ChooseAccountCreationButton") } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt index b57b3626e3c..b39a565f3da 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt @@ -7,7 +7,6 @@ import e2e.pageObjects.settings.accountSecurity.AccountSecurityPage class SettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - // UI Elements private val accountSecurityButton by lazy { getElement("AccountSecuritySettingsButton") } /** diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt index 3b0ae3165a2..5349078d30a 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt @@ -13,7 +13,6 @@ import e2e.pageObjects.vault.UnlockVaultPage */ class AccountSecurityPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - // UI Elements private val lockNowLabel by lazy { getElement("LockNowLabel") } /** diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt index 31df168d033..93516e3c983 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt @@ -8,7 +8,6 @@ import e2e.pageObjects.Page class UnlockVaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - // UI Elements private val passwordEntryTag by lazy { getElement("MasterPasswordEntry") } private val unlockVaultButtonTag by lazy { getElement("UnlockVaultButton") } diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt b/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt index a146e4e4a41..f186f91f482 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt +++ b/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt @@ -8,7 +8,6 @@ import e2e.pageObjects.settings.SettingsPage class VaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { - // UI Elements private val settingsMenuButton by lazy { getElement("SettingsTab") } private val addItemButton by lazy { getElement("AddItemButton") } From a6efb311fa61a9da1320680fd40f439755e5fa0b Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Wed, 23 Jul 2025 08:56:15 -0300 Subject: [PATCH 33/37] Adding package structure to androidTest folders --- .../kotlin/{ => com/x8bit/bitwarden}/data/TestData.kt | 2 +- .../{ => com/x8bit/bitwarden}/data/TestDataReader.kt | 3 +-- .../{ => com/x8bit/bitwarden}/e2e/pageObjects/Page.kt | 2 +- .../e2e/pageObjects/login/EnvironmentSettingsPage.kt | 5 ++--- .../x8bit/bitwarden}/e2e/pageObjects/login/LoginPage.kt | 7 +++---- .../x8bit/bitwarden}/e2e/pageObjects/login/MainPage.kt | 5 ++--- .../bitwarden}/e2e/pageObjects/settings/SettingsPage.kt | 6 +++--- .../settings/accountSecurity/AccountSecurityPage.kt | 6 +++--- .../bitwarden}/e2e/pageObjects/vault/UnlockVaultPage.kt | 4 ++-- .../x8bit/bitwarden}/e2e/pageObjects/vault/VaultPage.kt | 6 +++--- .../{ => com/x8bit/bitwarden}/e2e/tests/BaseE2eTest.kt | 4 ++-- .../x8bit/bitwarden}/e2e/tests/RealDeviceE2eTests.kt | 4 ++-- 12 files changed, 25 insertions(+), 29 deletions(-) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/data/TestData.kt (82%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/data/TestDataReader.kt (93%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/Page.kt (98%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/login/EnvironmentSettingsPage.kt (85%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/login/LoginPage.kt (93%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/login/MainPage.kt (81%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/settings/SettingsPage.kt (75%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt (82%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/vault/UnlockVaultPage.kt (89%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/pageObjects/vault/VaultPage.kt (78%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/tests/BaseE2eTest.kt (86%) rename app/src/androidTest/kotlin/{ => com/x8bit/bitwarden}/e2e/tests/RealDeviceE2eTests.kt (88%) diff --git a/app/src/androidTest/kotlin/data/TestData.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestData.kt similarity index 82% rename from app/src/androidTest/kotlin/data/TestData.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestData.kt index 7ee6816f604..173044e9fb5 100644 --- a/app/src/androidTest/kotlin/data/TestData.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestData.kt @@ -1,4 +1,4 @@ -package data +package com.x8bit.bitwarden.data import kotlinx.serialization.Serializable diff --git a/app/src/androidTest/kotlin/data/TestDataReader.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestDataReader.kt similarity index 93% rename from app/src/androidTest/kotlin/data/TestDataReader.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestDataReader.kt index a09b62695f9..3cbd257cb4e 100644 --- a/app/src/androidTest/kotlin/data/TestDataReader.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestDataReader.kt @@ -1,8 +1,7 @@ -package data +package com.x8bit.bitwarden.data import androidx.test.platform.app.InstrumentationRegistry import kotlinx.serialization.json.Json -import java.io.InputStream import java.nio.charset.StandardCharsets object TestDataReader { diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/Page.kt similarity index 98% rename from app/src/androidTest/kotlin/e2e/pageObjects/Page.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/Page.kt index 6d0d56d3517..edb6b499ebb 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/Page.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/Page.kt @@ -1,4 +1,4 @@ -package e2e.pageObjects +package com.x8bit.bitwarden.e2e.pageObjects import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertIsDisplayed diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/EnvironmentSettingsPage.kt similarity index 85% rename from app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/EnvironmentSettingsPage.kt index a26db96d405..7f10f9bcc31 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/EnvironmentSettingsPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/EnvironmentSettingsPage.kt @@ -1,10 +1,9 @@ -package e2e.pageObjects.login +package com.x8bit.bitwarden.e2e.pageObjects.login import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput -import com.x8bit.bitwarden.e2e.pages.LoginPage -import e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.Page class EnvironmentSettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/LoginPage.kt similarity index 93% rename from app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/LoginPage.kt index 20d28939fa0..c1fdc7d666f 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/LoginPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/LoginPage.kt @@ -1,11 +1,10 @@ -package com.x8bit.bitwarden.e2e.pages +package com.x8bit.bitwarden.e2e.pageObjects.login import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performClick import androidx.compose.ui.test.junit4.ComposeTestRule -import e2e.pageObjects.Page -import e2e.pageObjects.login.EnvironmentSettingsPage -import e2e.pageObjects.vault.VaultPage +import com.x8bit.bitwarden.e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.vault.VaultPage /** * Page Object representing the Login screen of the Bitwarden app. diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/MainPage.kt similarity index 81% rename from app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/MainPage.kt index 0ffac2102ba..3a788ad8a25 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/login/MainPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/login/MainPage.kt @@ -1,9 +1,8 @@ -package e2e.pageObjects.login +package com.x8bit.bitwarden.e2e.pageObjects.login import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.performClick -import com.x8bit.bitwarden.e2e.pages.LoginPage -import e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.Page class MainPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/settings/SettingsPage.kt similarity index 75% rename from app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/settings/SettingsPage.kt index b39a565f3da..f7243d3456b 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/SettingsPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/settings/SettingsPage.kt @@ -1,9 +1,9 @@ -package e2e.pageObjects.settings +package com.x8bit.bitwarden.e2e.pageObjects.settings import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.performClick -import e2e.pageObjects.Page -import e2e.pageObjects.settings.accountSecurity.AccountSecurityPage +import com.x8bit.bitwarden.e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.settings.accountSecurity.AccountSecurityPage class SettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt similarity index 82% rename from app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt index 5349078d30a..751d13787a0 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/settings/accountSecurity/AccountSecurityPage.kt @@ -1,11 +1,11 @@ -package e2e.pageObjects.settings.accountSecurity +package com.x8bit.bitwarden.e2e.pageObjects.settings.accountSecurity import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performClick import androidx.compose.ui.test.junit4.ComposeTestRule -import e2e.pageObjects.Page -import e2e.pageObjects.vault.UnlockVaultPage +import com.x8bit.bitwarden.e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.vault.UnlockVaultPage /** * Page Object representing the Account Security screen of the Bitwarden app. diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/vault/UnlockVaultPage.kt similarity index 89% rename from app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/vault/UnlockVaultPage.kt index 93516e3c983..d0098ffecc8 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/UnlockVaultPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/vault/UnlockVaultPage.kt @@ -1,10 +1,10 @@ -package e2e.pageObjects.vault +package com.x8bit.bitwarden.e2e.pageObjects.vault import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput -import e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.Page class UnlockVaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { diff --git a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/vault/VaultPage.kt similarity index 78% rename from app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/vault/VaultPage.kt index f186f91f482..b8da915b629 100644 --- a/app/src/androidTest/kotlin/e2e/pageObjects/vault/VaultPage.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/pageObjects/vault/VaultPage.kt @@ -1,10 +1,10 @@ -package e2e.pageObjects.vault +package com.x8bit.bitwarden.e2e.pageObjects.vault import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.performClick -import e2e.pageObjects.Page -import e2e.pageObjects.settings.SettingsPage +import com.x8bit.bitwarden.e2e.pageObjects.Page +import com.x8bit.bitwarden.e2e.pageObjects.settings.SettingsPage class VaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) { diff --git a/app/src/androidTest/kotlin/e2e/tests/BaseE2eTest.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/tests/BaseE2eTest.kt similarity index 86% rename from app/src/androidTest/kotlin/e2e/tests/BaseE2eTest.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/tests/BaseE2eTest.kt index 7a6e5a34606..9d8c949efc7 100644 --- a/app/src/androidTest/kotlin/e2e/tests/BaseE2eTest.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/tests/BaseE2eTest.kt @@ -1,10 +1,10 @@ -package e2e.tests +package com.x8bit.bitwarden.e2e.tests import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.test.ext.junit.rules.ActivityScenarioRule import com.x8bit.bitwarden.MainActivity -import data.TestDataReader +import com.x8bit.bitwarden.data.TestDataReader import org.junit.Rule open class BaseE2eTest { diff --git a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/tests/RealDeviceE2eTests.kt similarity index 88% rename from app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt rename to app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/tests/RealDeviceE2eTests.kt index 77c99a38cc7..8ea32cf45d5 100644 --- a/app/src/androidTest/kotlin/e2e/tests/RealDeviceE2eTests.kt +++ b/app/src/androidTest/kotlin/com/x8bit/bitwarden/e2e/tests/RealDeviceE2eTests.kt @@ -1,7 +1,7 @@ -package e2e.tests +package com.x8bit.bitwarden.e2e.tests import androidx.test.ext.junit.runners.AndroidJUnit4 -import e2e.pageObjects.login.MainPage +import com.x8bit.bitwarden.e2e.pageObjects.login.MainPage import org.junit.Test import org.junit.runner.RunWith From f803752861b802d2be3da4371fe28aae83bf5418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Thu, 31 Jul 2025 17:29:02 +0100 Subject: [PATCH 34/37] Refactor - move test apk assembly to build job --- .github/workflows/build.yml | 201 +++++++++--------------------- .github/workflows/test-device.yml | 150 +++++++--------------- fastlane/Fastfile | 13 ++ 3 files changed, 114 insertions(+), 250 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 86db4076aa2..87c9c11fc18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -142,7 +142,7 @@ jobs: uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: gh-android - secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD" + secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD,BWS-ACCESS-TOKEN" - name: Retrieve secrets env: @@ -260,6 +260,48 @@ jobs: keyAlias:bitwarden \ keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }} + - name: Retrieve test data + if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} + uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 + with: + access_token: ${{ steps.get-kv-secrets.outputs.BWS-ACCESS-TOKEN }} + secrets: | + 63e93f73-5118-4a62-9db8-b3160176aa8a > TEST_ACCOUNT_CREDS + + - name: Configure .json test data file + run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json + + - name: Build test APK (espresso) + if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} + env: + _TEST_APK_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + _TEST_APK_SIGNED_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk + run: | + ./gradlew :app:assembleStandardReleaseAndroidTest + # bundle exec fastlane assembleTestApk \ + # storeFile:app_play-keystore.jks \ + # storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \ + # keyAlias:bitwarden \ + # keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }} + # mv $_TEST_APK_PATH $_TEST_APK_SIGNED_PATH + + # TODO: test if bundle exec fastlane assembleTestApk works and replace this step + - name: Sign and rename test APK + if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} + env: + _TEST_APK_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + _TEST_APK_SIGNED_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk + _PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }} + _PLAY_KEYSTORE_ALIAS: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-ALIAS }} + run: | + $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ + --ks keystores/app_play-keystore.jks \ + --ks-key-alias bitwarden \ + --ks-pass pass:$_PLAY_KEYSTORE_PASSWORD \ + --key-pass pass:$_PLAY_KEYSTORE_PASSWORD \ + $_TEST_APK_PATH + mv $_TEST_APK_PATH $_TEST_APK_SIGNED_PATH + - name: Generate beta Play Store APK if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} env: @@ -301,6 +343,14 @@ jobs: path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk if-no-files-found: error + - name: Upload test .apk artifact + if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: com.x8bit.bitwarden-test.apk + path: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk + if-no-files-found: error + - name: Upload beta .apk artifact if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -421,6 +471,14 @@ jobs: bundle exec fastlane publishProdToPlayStore bundle exec fastlane publishBetaToPlayStore + test-device: + name: Test device + needs: publish_playstore + uses: bitwarden/android/.github/workflows/test-device.yml@QA-1126b/adding-native-sanity-test #TODO replace branch with main before merging + with: + apk_filename: com.x8bit.bitwarden.apk + test_apk_filename: com.x8bit.bitwarden-test.apk + publish_fdroid: name: Publish F-Droid artifacts needs: @@ -602,144 +660,3 @@ jobs: bundle exec fastlane distributeReleaseFDroidToFirebase \ actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \ service_credentials_file:${{ env.APP_FDROID_FIREBASE_CREDS_PATH }} - - test_on_device: - name: Running sanity check on a real device - needs: publish_playstore - runs-on: ubuntu-24.04 - if: github.ref == 'refs/heads/main' - env: - _JAVA_VERSION: 17 - _GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - steps: - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 - - - name: Cache Gradle files - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }} - restore-keys: | - ${{ runner.os }}-gradle-v2- - - - name: Cache build output - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ${{ github.workspace }}/build-cache - key: ${{ runner.os }}-build-cache-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-build- - - - name: Configure JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: - distribution: "temurin" - java-version: ${{ env._JAVA_VERSION }} - - - name: Configure Ruby - uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0 - with: - bundler-cache: true - - - name: Install Fastlane - run: | - gem install bundler:2.2.27 - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - - name: Install saucectl - run: | - npm i -g saucectl - - - name: Retrieve secrets - env: - ACCOUNT_NAME: bitwardenci - CONTAINER_NAME: mobile - run: | - mkdir -p ${{ github.workspace }}/secrets - mkdir -p ${{ github.workspace }}/app/src/standardRelease - - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ - --name app_play-keystore.jks --file ${{ github.workspace }}/keystores/app_play-keystore.jks --output none - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ - --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none - - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - - name: Get E2E test secrets from Azure - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: gh-android - secrets: "BWS-ACCESS-TOKEN, SAUCE-LABS-USERNAME, SAUCE-LABS-ACCESS-KEY" - id: get-e2e-secrets - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - - name: Retrieve test data - uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 - with: - access_token: ${{ steps.get-e2e-secrets.outputs.BWS-ACCESS-TOKEN }} - secrets: | - 63e93f73-5118-4a62-9db8-b3160176aa8a > TEST_ACCOUNT_CREDS - - - name: Configure .json test data file - run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json - - - name: Build test APK (espresso) - run: | - ./gradlew :app:assembleStandardReleaseAndroidTest - - - name: Download release APK artifact - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: com.x8bit.bitwarden.apk - path: app/build/outputs/apk/standard/release/ - - - name: Signing test APK - run: | - $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ - --ks keystores/app_play-keystore.jks \ - --ks-key-alias bitwarden \ - --ks-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ - --key-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ - app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk - - - name: Upload APK to SauceLabs storage - run: | - saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk - env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - - - name: Upload test APK to SauceLabs storage - run: | - saucectl storage upload app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk - env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - - - name: Run tests on SauceLabs - run: saucectl run --config .sauce/config.yml - env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - - - name: Upload SauceLabs test report - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: saucectl-report - path: saucectl-report.xml diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index bacd2cdf08c..a28a51462f1 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -1,8 +1,22 @@ name: Test Device on: - workflow_dispatch: - + workflow_call: + inputs: + apk_filename: + type: string + description: "Filename of the APK file to test" + default: com.x8bit.bitwarden.apk + test_apk_filename: + type: string + description: "Filename of the test APK file to test" + default: com.x8bit.bitwarden-test.apk + +env: + _APK_PATH: artifacts/${{ inputs.apk_filename }} + _TEST_APK_PATH: artifacts/${{ inputs.test_apk_filename }} + +# TODO confirm if these permissions are needed permissions: contents: read actions: read @@ -13,74 +27,11 @@ jobs: test-device: name: Check main build against real devices runs-on: ubuntu-24.04 - env: - _JAVA_VERSION: 17 - _GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # TODO: do we need this? + # env: + # _GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 - - - name: Cache Gradle files - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }} - restore-keys: | - ${{ runner.os }}-gradle-v2- - - - name: Cache build output - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ${{ github.workspace }}/build-cache - key: ${{ runner.os }}-build-cache-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-build- - - - name: Configure JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: - distribution: "temurin" - java-version: ${{ env.JAVA_VERSION }} - - - name: Configure Ruby - uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0 - with: - bundler-cache: true - - - name: Install Fastlane - run: | - gem install bundler:2.2.27 - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - - name: Install saucectl - run: | - npm i -g saucectl - - - name: Log in to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - env: - ACCOUNT_NAME: bitwardenci - CONTAINER_NAME: mobile - run: | - mkdir -p ${{ github.workspace }}/secrets - mkdir -p ${{ github.workspace }}/app/src/standardRelease - - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ - --name app_play-keystore.jks --file ${{ github.workspace }}/keystores/app_play-keystore.jks --output none - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ - --name google-services.json --file ${{ github.workspace }}/app/src/standardRelease/google-services.json --output none - - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main with: @@ -92,64 +43,47 @@ jobs: uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: gh-android - secrets: "BWS-ACCESS-TOKEN, SAUCE-LABS-USERNAME, SAUCE-LABS-ACCESS-KEY" - id: get-e2e-secrets + secrets: "SAUCE-LABS-USERNAME,SAUCE-LABS-ACCESS-KEY" + id: get-kv-secrets - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main - - name: Retrieve test data - uses: bitwarden/sm-action@14f92f1d294ae3c2b6a3845d389cd2c318b0dfd8 # v2.2.0 + - name: Download release APK artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - access_token: ${{ steps.get-e2e-secrets.outputs.BWS-ACCESS-TOKEN }} - secrets: | - 63e93f73-5118-4a62-9db8-b3160176aa8a > TEST_ACCOUNT_CREDS + name: ${{ inputs.apk_filename }} + path: artifacts - - name: Configure .json test data file - run: printf %s '${{ env.TEST_ACCOUNT_CREDS }}' > app/src/androidTest/assets/TestData.json - - - name: Build release APK - env: - PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} - run: | - bundle exec fastlane assemblePlayStoreReleaseApk \ - storeFile:app_play-keystore.jks \ - storePassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' \ - keyAlias:bitwarden \ - keyPassword:'${{ env.PLAY_KEYSTORE_PASSWORD }}' - - - name: Build test APK (espresso) - run: | - ./gradlew :app:assembleStandardReleaseAndroidTest + - name: Download test APK artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: ${{ inputs.test_apk_filename }} + path: artifacts - - name: Signing test APK + - name: Install saucectl run: | - $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ - --ks keystores/app_play-keystore.jks \ - --ks-key-alias bitwarden \ - --ks-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ - --key-pass pass:${{ secrets.PLAY_KEYSTORE_PASSWORD }} \ - app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + npm i -g saucectl - - name: Upload app APK to SauceLabs storage + - name: Upload APK to SauceLabs storage run: | - saucectl storage upload app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk + saucectl storage upload $_APK_PATH env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + SAUCE_USERNAME: ${{ steps.get-kv-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-kv-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Upload test APK to SauceLabs storage - run: | - saucectl storage upload app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + SAUCE_USERNAME: ${{ steps.get-kv-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-kv-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + run: | + saucectl storage upload $_TEST_APK_PATH - name: Run tests on SauceLabs run: saucectl run --config .sauce/config.yml env: - SAUCE_USERNAME: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-USERNAME }} - SAUCE_ACCESS_KEY: ${{ steps.get-e2e-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} + SAUCE_USERNAME: ${{ steps.get-kv-secrets.outputs.SAUCE-LABS-USERNAME }} + SAUCE_ACCESS_KEY: ${{ steps.get-kv-secrets.outputs.SAUCE-LABS-ACCESS-KEY }} - name: Upload SauceLabs test report if: always() diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 803c0be24e1..c2bcf14098b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -69,6 +69,19 @@ platform :android do ) end + # TODO: test if lane builds and signs correctly + desc "Assemble test APK" + lane :assembleTestApk do |options| + buildAndSignBitwarden( + taskName: "app:assembleStandardReleaseAndroidTest", + buildType: "Release", + storeFile: options[:storeFile], + storePassword: options[:storePassword], + keyAlias: options[:keyAlias], + keyPassword: options[:keyPassword], + ) + end + desc "Assemble Play Store release APK" lane :assemblePlayStoreBetaApk do |options| buildAndSignBitwarden( From bb8dda4442d5ac6bea2e91c26011d4d9bc147e06 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Mon, 4 Aug 2025 14:46:01 -0300 Subject: [PATCH 35/37] Adding missing permissions to test-device workflow call --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87c9c11fc18..ee26473b921 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -478,6 +478,11 @@ jobs: with: apk_filename: com.x8bit.bitwarden.apk test_apk_filename: com.x8bit.bitwarden-test.apk + permissions: + actions: read + checks: write + contents: read + id-token: write publish_fdroid: name: Publish F-Droid artifacts From 12edccc4b3dea00b345301403a0427db79f45dd7 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 8 Aug 2025 09:58:52 -0300 Subject: [PATCH 36/37] Moving test-app creation to fastlane --- .github/workflows/build.yml | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee26473b921..657865e1990 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -278,29 +278,29 @@ jobs: _TEST_APK_SIGNED_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk run: | ./gradlew :app:assembleStandardReleaseAndroidTest - # bundle exec fastlane assembleTestApk \ - # storeFile:app_play-keystore.jks \ - # storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \ - # keyAlias:bitwarden \ - # keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }} - # mv $_TEST_APK_PATH $_TEST_APK_SIGNED_PATH + bundle exec fastlane assembleTestApk \ + storeFile:app_play-keystore.jks \ + storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \ + keyAlias:bitwarden \ + keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }} + mv $_TEST_APK_PATH $_TEST_APK_SIGNED_PATH # TODO: test if bundle exec fastlane assembleTestApk works and replace this step - - name: Sign and rename test APK - if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} - env: - _TEST_APK_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk - _TEST_APK_SIGNED_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk - _PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }} - _PLAY_KEYSTORE_ALIAS: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-ALIAS }} - run: | - $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ - --ks keystores/app_play-keystore.jks \ - --ks-key-alias bitwarden \ - --ks-pass pass:$_PLAY_KEYSTORE_PASSWORD \ - --key-pass pass:$_PLAY_KEYSTORE_PASSWORD \ - $_TEST_APK_PATH - mv $_TEST_APK_PATH $_TEST_APK_SIGNED_PATH + # - name: Sign and rename test APK + # if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} + # env: + # _TEST_APK_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk + # _TEST_APK_SIGNED_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk + # _PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }} + # _PLAY_KEYSTORE_ALIAS: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-ALIAS }} + # run: | + # $ANDROID_SDK_ROOT/build-tools/34.0.0/apksigner sign \ + # --ks keystores/app_play-keystore.jks \ + # --ks-key-alias bitwarden \ + # --ks-pass pass:$_PLAY_KEYSTORE_PASSWORD \ + # --key-pass pass:$_PLAY_KEYSTORE_PASSWORD \ + # $_TEST_APK_PATH + # mv $_TEST_APK_PATH $_TEST_APK_SIGNED_PATH - name: Generate beta Play Store APK if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }} From da9b60f5edda00ea75d4ad8a21ac5a61f0f54b57 Mon Sep 17 00:00:00 2001 From: ifernandezdiaz Date: Fri, 8 Aug 2025 11:51:48 -0300 Subject: [PATCH 37/37] Fixing build issues --- .github/workflows/build.yml | 1 - .github/workflows/test-device.yml | 3 --- fastlane/Fastfile | 5 ++--- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9fbc5eeb8e9..a496676b544 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -278,7 +278,6 @@ jobs: _TEST_APK_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-standard-release-androidTest.apk _TEST_APK_SIGNED_PATH: app/build/outputs/apk/androidTest/standard/release/com.x8bit.bitwarden-test.apk run: | - ./gradlew :app:assembleStandardReleaseAndroidTest bundle exec fastlane assembleTestApk \ storeFile:app_play-keystore.jks \ storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \ diff --git a/.github/workflows/test-device.yml b/.github/workflows/test-device.yml index a28a51462f1..fcae2698981 100644 --- a/.github/workflows/test-device.yml +++ b/.github/workflows/test-device.yml @@ -27,9 +27,6 @@ jobs: test-device: name: Check main build against real devices runs-on: ubuntu-24.04 - # TODO: do we need this? - # env: - # _GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Log in to Azure diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 133764bacaf..8257536737f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -69,11 +69,10 @@ platform :android do ) end - # TODO: test if lane builds and signs correctly - desc "Assemble test APK" + desc "Assemble test APK" lane :assembleTestApk do |options| buildAndSignBitwarden( - taskName: "app:assembleStandardReleaseAndroidTest", + taskName: "assembleAndroidTest", buildType: "Release", storeFile: options[:storeFile], storePassword: options[:storePassword],