Compare commits

...

52 Commits

Author SHA1 Message Date
ifernandezdiaz
0aafc52231 Updating branch 2025-09-30 09:05:54 -03:00
ifernandezdiaz
9bf3d1ed0d Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-08-11 11:06:36 -03:00
ifernandezdiaz
da9b60f5ed Fixing build issues 2025-08-08 13:57:34 -03:00
ifernandezdiaz
15b5b86b34 Updating branch 2025-08-08 10:00:25 -03:00
ifernandezdiaz
12edccc4b3 Moving test-app creation to fastlane 2025-08-08 09:58:52 -03:00
ifernandezdiaz
bb8dda4442 Adding missing permissions to test-device workflow call 2025-08-04 14:47:45 -03:00
Álison Fernandes
f803752861 Refactor - move test apk assembly to build job 2025-07-31 17:32:19 +01:00
ifernandezdiaz
ab481d29eb Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-28 11:44:57 -03:00
ifernandezdiaz
b74d3b1caa Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-23 09:18:49 -03:00
ifernandezdiaz
a6efb311fa Adding package structure to androidTest folders 2025-07-23 08:56:15 -03:00
ifernandezdiaz
b1a74c6fae Adding suggestions 2025-07-22 16:52:53 -03:00
ifernandezdiaz
b6e90e487b Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-22 12:04:14 -03:00
ifernandezdiaz
be325eb43c Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-21 15:19:39 -03:00
ifernandezdiaz
c4ebde786c Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-18 11:12:40 -03:00
ifernandezdiaz
7a985557aa fixing static analisys issues 2025-07-18 11:09:40 -03:00
ifernandezdiaz
62b9088a5a Fixing lint issues 2025-07-15 12:47:49 -03:00
ifernandezdiaz
6be753b4ed Updating branch 2025-07-15 12:02:33 -03:00
ifernandezdiaz
bbd7e7c00d Adding suggestions 2025-07-15 09:49:33 -03:00
ifernandezdiaz
909b0d03cb Integrating e2e test run unto build workflow 2025-07-14 16:59:30 -03:00
ifernandezdiaz
7dc5b99342 Fix formatting 2025-07-14 16:38:33 -03:00
ifernandezdiaz
47a9117590 Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-14 16:31:48 -03:00
ifernandezdiaz
9474133622 Restoring build steps 2025-07-14 16:30:34 -03:00
ifernandezdiaz
cab4461bcf Decreasing timeout 2025-07-14 16:10:16 -03:00
ifernandezdiaz
d3d02f4a10 Increasing timeout 2025-07-14 15:52:08 -03:00
ifernandezdiaz
85f0bd920c Removing steps to test faster on Device Farm 2025-07-14 15:41:09 -03:00
ifernandezdiaz
7bedb1f4fd Adding steps to enable screen recording 2025-07-14 15:10:42 -03:00
ifernandezdiaz
e20c8149e8 Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-14 12:35:57 -03:00
ifernandezdiaz
a5721f3e59 Updating the way we read json data 2025-07-11 19:53:53 -03:00
ifernandezdiaz
9a724d805b Fixing importing issues during test execution 2025-07-11 18:25:16 -03:00
ifernandezdiaz
c6f8fe2431 Signing testApp 2025-07-11 17:37:25 -03:00
ifernandezdiaz
a6bd9b6dda Signing testApp 2025-07-11 15:59:16 -03:00
ifernandezdiaz
e9afd92912 Signing testApp 2025-07-11 13:42:35 -03:00
ifernandezdiaz
5fc3c0227e Fixing testApp filename 2025-07-11 12:03:52 -03:00
ifernandezdiaz
22644ab21c Fixing SauceLabs creds retrieval 2025-07-11 11:42:39 -03:00
ifernandezdiaz
6b2ac29655 Revert "Pulling SauceLabs creds from GH secrets"
This reverts commit 4b2f0da0ae.
2025-07-11 10:08:09 -03:00
ifernandezdiaz
4b2f0da0ae Pulling SauceLabs creds from GH secrets 2025-07-11 08:37:47 -03:00
ifernandezdiaz
b8172f9c4b Installing saucectl via npm 2025-07-10 20:47:17 -03:00
ifernandezdiaz
a371efbd6b Adding testData file 2025-07-10 19:51:48 -03:00
ifernandezdiaz
26cb4cedfe Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-07-10 19:45:31 -03:00
ifernandezdiaz
b0fb07110d Updating Azure creds to pull SM secrets 2025-07-10 19:30:46 -03:00
ifernandezdiaz
a44a8dc6d4 Pulling SM creds from Azure 2025-07-01 11:26:17 -03:00
ifernandezdiaz
58c98fa297 Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-06-30 16:57:17 -03:00
ifernandezdiaz
2856545888 Enabling release test build type 2025-06-30 16:56:32 -03:00
ifernandezdiaz
a2ed706625 Adding missing steps to retrieve secrets from Azure 2025-06-27 14:34:07 -03:00
ifernandezdiaz
772245cfc5 Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-06-27 11:36:08 -03:00
ifernandezdiaz
6181378f28 Adding missing steps to test-device workflow 2025-06-27 10:26:44 -03:00
ifernandezdiaz
2af019e555 Updating code to read credentials from an external json file 2025-06-26 15:15:53 -03:00
ifernandezdiaz
047b762912 Merge branch 'main' into QA-1126b/adding-native-sanity-test 2025-06-26 10:39:09 -03:00
ifernandezdiaz
a6ef2ea78d Using Compose Testing + Espresso 2025-06-26 10:35:41 -03:00
ifernandezdiaz
5b012e1d23 Implementing page object pattern 2025-06-10 14:28:10 -03:00
ifernandezdiaz
ed5b752717 Creating base e2e test for Real Device validations 2025-06-09 16:10:23 -03:00
ifernandezdiaz
815c73944f Creating base e2e test for Real Device validations 2025-06-09 16:06:36 -03:00
22 changed files with 639 additions and 8 deletions

View File

@@ -143,7 +143,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:
@@ -261,6 +261,47 @@ 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: |
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:
@@ -302,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
@@ -422,6 +471,19 @@ 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
permissions:
actions: read
checks: write
contents: read
id-token: write
publish_fdroid:
name: Publish F-Droid artifacts
needs:

View File

@@ -1,16 +1,90 @@
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
checks: write
id-token: write
jobs:
test:
name: Test Device
test-device:
name: Check main build against real devices
runs-on: ubuntu-24.04
steps:
- name: Placeholder step
run: echo "Placeholder workflow step"
- 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
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
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: Download release APK artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ inputs.apk_filename }}
path: artifacts
- name: Download test APK artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ inputs.test_apk_filename }}
path: artifacts
- name: Install saucectl
run: |
npm i -g saucectl
- name: Upload APK to SauceLabs storage
run: |
saucectl storage upload $_APK_PATH
env:
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
env:
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-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()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: saucectl-report
path: saucectl-report.xml

32
.sauce/config.yml Normal file
View File

@@ -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-standard-release-androidTest.apk
suites:
- name: "Android - Sanity"
devices:
- name: "Google.*"
platformVersion: "^1[3456].*"
options:
deviceType: PHONE
testOptions:
package: e2e.tests
resigningEnabled: false

View File

@@ -279,6 +279,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.

View File

@@ -47,6 +47,9 @@ android {
namespace = "com.x8bit.bitwarden"
compileSdk = libs.versions.compileSdk.get().toInt()
// Required for SauceLabs integration
testBuildType = "release"
room {
schemaDirectory("$projectDir/schemas")
}
@@ -253,6 +256,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)
@@ -288,7 +295,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)
@@ -298,6 +304,11 @@ dependencies {
testImplementation(libs.mockk.mockk)
testImplementation(libs.robolectric.robolectric)
testImplementation(libs.square.turbine)
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.uiautomator)
}
tasks {

View File

@@ -121,3 +121,23 @@
-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.** { *; }
# Keep Compose test classes
-keep class androidx.compose.ui.test.** { *; }
-keep class androidx.compose.ui.test.junit4.** { *; }
# Keep Kotlin standard library classes
-keep class kotlin.** { *; }
-keep class kotlinx.** { *; }
-keep class kotlin.io.** { *; }

View File

@@ -0,0 +1,5 @@
{
"baseUrl": "_",
"email": "_",
"password": "_"
}

View File

@@ -0,0 +1,10 @@
package com.x8bit.bitwarden.data
import kotlinx.serialization.Serializable
@Serializable
data class TestData(
val baseUrl: String,
val email: String,
val password: String,
)

View File

@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.serialization.json.Json
import java.nio.charset.StandardCharsets
object TestDataReader {
fun getTestData(fileName: String): TestData {
val assets = InstrumentationRegistry.getInstrumentation().context.assets
val jsonString = assets
.open(fileName)
.use { inputStream ->
inputStream.bufferedReader(StandardCharsets.UTF_8)
.readText()
}
return Json.decodeFromString<TestData>(jsonString)
}
}

View File

@@ -0,0 +1,122 @@
package com.x8bit.bitwarden.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 {
const val TIMEOUT_MILLIS = 30000L
}
/**
* 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
*/
protected fun getElement(testTag: String): SemanticsNodeInteraction {
waitForIdle()
waitUntil() {
try {
composeTestRule.onNodeWithTag(testTag).assertExists()
true
} catch (e: AssertionError) {
false
}
}
return composeTestRule.onNodeWithTag(testTag)
}
protected fun getElementByText(text: String): SemanticsNodeInteraction {
waitForIdle()
waitUntil() {
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 = TIMEOUT_MILLIS,
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)
}
}

View File

@@ -0,0 +1,20 @@
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.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)
}
}

View File

@@ -0,0 +1,60 @@
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 com.x8bit.bitwarden.e2e.pageObjects.Page
import com.x8bit.bitwarden.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) {
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") }
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
* @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)
}
fun turnOnScreenRecording(): LoginPage {
openSettingsButton.performClick()
otherSettingsButton.performClick()
allowScreenCaptureToggle.performClick()
goBackButton.performClick()
goBackButton.performClick()
return this
}
}

View File

@@ -0,0 +1,16 @@
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.pageObjects.Page
class MainPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) {
private val loginButton by lazy { getElement("ChooseLoginButton") }
private val createAccountButton by lazy { getElement("ChooseAccountCreationButton") }
fun startLogin(): LoginPage {
loginButton.performClick()
return LoginPage(composeTestRule)
}
}

View File

@@ -0,0 +1,20 @@
package com.x8bit.bitwarden.e2e.pageObjects.settings
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import com.x8bit.bitwarden.e2e.pageObjects.Page
import com.x8bit.bitwarden.e2e.pageObjects.settings.accountSecurity.AccountSecurityPage
class SettingsPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) {
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)
}
}

View File

@@ -0,0 +1,27 @@
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 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.
* This class encapsulates all the UI elements and actions available on the account security screen.
*/
class AccountSecurityPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) {
private val lockNowLabel by lazy { getElement("LockNowLabel") }
/**
* Locks the vault
* @return This AccountSecurityPage instance for method chaining
*/
fun lockVault(): UnlockVaultPage {
lockNowLabel.performScrollTo().performClick()
lockNowLabel.assertIsNotDisplayed()
return UnlockVaultPage(composeTestRule)
}
}

View File

@@ -0,0 +1,25 @@
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 com.x8bit.bitwarden.e2e.pageObjects.Page
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
}
fun performUnlockVault(password: String): VaultPage {
unlockVaultButtonTag.assertIsDisplayed()
passwordEntryTag.performClick().performTextInput(password)
unlockVaultButtonTag.performClick()
return VaultPage(composeTestRule)
}
}

View File

@@ -0,0 +1,22 @@
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 com.x8bit.bitwarden.e2e.pageObjects.Page
import com.x8bit.bitwarden.e2e.pageObjects.settings.SettingsPage
class VaultPage(composeTestRule: ComposeTestRule) : Page(composeTestRule) {
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)
}
}

View File

@@ -0,0 +1,20 @@
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 com.x8bit.bitwarden.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")
}

View File

@@ -0,0 +1,26 @@
package com.x8bit.bitwarden.e2e.tests
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.x8bit.bitwarden.e2e.pageObjects.login.MainPage
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RealDeviceE2eTests : BaseE2eTest() {
@Test
fun testVaultLockUnlockFlow() {
var vault = MainPage(composeTestRule)
.startLogin()
.turnOnScreenRecording()
.openEnvironmentSettings()
.setupEnvironment(testData.baseUrl)
.performLogin(testData.email, testData.password)
vault.assertVaultIsUnlocked()
vault.navigateToSettingsPage()
.navigateToAccountSecurity()
.lockVault()
.performUnlockVault(testData.password)
.assertVaultIsUnlocked()
}
}

View File

@@ -324,7 +324,8 @@ private fun LandingScreenContent(
icon = rememberVectorPainter(id = BitwardenDrawable.ic_cog),
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
.fillMaxWidth()
.testTag("AppSettingsButton"),
)
Spacer(modifier = Modifier.height(height = 12.dp))

View File

@@ -69,6 +69,18 @@ platform :android do
)
end
desc "Assemble test APK"
lane :assembleTestApk do |options|
buildAndSignBitwarden(
taskName: "assembleAndroidTest",
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(

View File

@@ -33,6 +33,7 @@ androidxWork = "2.10.5"
bitwardenSdk = "1.0.0-3175-c9758478"
crashlytics = "3.0.6"
detekt = "1.23.8"
espressoCore = "3.6.1"
firebaseBom = "34.2.0"
glide = "1.0.0-beta01"
googleGuava = "33.4.8-jre"
@@ -42,6 +43,7 @@ googleServices = "4.4.3"
googleReview = "2.0.2"
hilt = "2.57.1"
junit5 = "5.13.4"
junitKtx = "1.2.1"
jvmTarget = "21"
# kotlin and ksp **must** use compatible versions, do not update either without the other.
kotlin = "2.2.20"
@@ -58,6 +60,7 @@ sonarqube = "6.2.0.5505"
testng = "7.11.0"
timber = "5.0.1"
turbine = "1.2.1"
uiautomator = "2.3.0"
zxing = "3.5.3"
[libraries]
@@ -86,7 +89,9 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx
androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "androidxCredentials" }
androidx-credentials-providerevents = { module = "androidx.credentials.providerevents:providerevents", version.ref = "androidxCredentialsProviderEvents" }
androidx-credentials-providerevents-play-services = { module = "androidx.credentials.providerevents:providerevents-play-services", version.ref = "androidxCredentialsProviderEvents" }
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" }
@@ -96,6 +101,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", version.ref = "bitwardenSdk" }
bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" }