Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
815c739
Creating base e2e test for Real Device validations
ifernandezdiaz Jun 9, 2025
ed5b752
Creating base e2e test for Real Device validations
ifernandezdiaz Jun 9, 2025
5b012e1
Implementing page object pattern
ifernandezdiaz Jun 10, 2025
a6ef2ea
Using Compose Testing + Espresso
ifernandezdiaz Jun 26, 2025
047b762
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jun 26, 2025
2af019e
Updating code to read credentials from an external json file
ifernandezdiaz Jun 26, 2025
6181378
Adding missing steps to test-device workflow
ifernandezdiaz Jun 27, 2025
772245c
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jun 27, 2025
a2ed706
Adding missing steps to retrieve secrets from Azure
ifernandezdiaz Jun 27, 2025
2856545
Enabling release test build type
ifernandezdiaz Jun 30, 2025
58c98fa
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jun 30, 2025
a44a8dc
Pulling SM creds from Azure
ifernandezdiaz Jul 1, 2025
b0fb071
Updating Azure creds to pull SM secrets
ifernandezdiaz Jul 1, 2025
26cb4ce
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 10, 2025
a371efb
Adding testData file
ifernandezdiaz Jul 10, 2025
b8172f9
Installing saucectl via npm
ifernandezdiaz Jul 10, 2025
4b2f0da
Pulling SauceLabs creds from GH secrets
ifernandezdiaz Jul 11, 2025
6b2ac29
Revert "Pulling SauceLabs creds from GH secrets"
ifernandezdiaz Jul 11, 2025
22644ab
Fixing SauceLabs creds retrieval
ifernandezdiaz Jul 11, 2025
5fc3c02
Fixing testApp filename
ifernandezdiaz Jul 11, 2025
e9afd92
Signing testApp
ifernandezdiaz Jul 11, 2025
a6bd9b6
Signing testApp
ifernandezdiaz Jul 11, 2025
c6f8fe2
Signing testApp
ifernandezdiaz Jul 11, 2025
9a724d8
Fixing importing issues during test execution
ifernandezdiaz Jul 11, 2025
a5721f3
Updating the way we read json data
ifernandezdiaz Jul 11, 2025
e20c814
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 14, 2025
7bedb1f
Adding steps to enable screen recording
ifernandezdiaz Jul 14, 2025
85f0bd9
Removing steps to test faster on Device Farm
ifernandezdiaz Jul 14, 2025
d3d02f4
Increasing timeout
ifernandezdiaz Jul 14, 2025
cab4461
Decreasing timeout
ifernandezdiaz Jul 14, 2025
9474133
Restoring build steps
ifernandezdiaz Jul 14, 2025
47a9117
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 14, 2025
7dc5b99
Fix formatting
ifernandezdiaz Jul 14, 2025
909b0d0
Integrating e2e test run unto build workflow
ifernandezdiaz Jul 14, 2025
bbd7e7c
Adding suggestions
ifernandezdiaz Jul 15, 2025
6be753b
Updating branch
ifernandezdiaz Jul 15, 2025
62b9088
Fixing lint issues
ifernandezdiaz Jul 15, 2025
7a98555
fixing static analisys issues
ifernandezdiaz Jul 18, 2025
c4ebde7
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 18, 2025
be325eb
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 21, 2025
b6e90e4
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 22, 2025
b1a74c6
Adding suggestions
ifernandezdiaz Jul 22, 2025
a6efb31
Adding package structure to androidTest folders
ifernandezdiaz Jul 23, 2025
b74d3b1
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 23, 2025
ab481d2
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Jul 28, 2025
f803752
Refactor - move test apk assembly to build job
vvolkgang Jul 31, 2025
bb8dda4
Adding missing permissions to test-device workflow call
ifernandezdiaz Aug 4, 2025
12edccc
Moving test-app creation to fastlane
ifernandezdiaz Aug 8, 2025
15b5b86
Updating branch
ifernandezdiaz Aug 8, 2025
da9b60f
Fixing build issues
ifernandezdiaz Aug 8, 2025
9bf3d1e
Merge branch 'main' into QA-1126b/adding-native-sanity-test
ifernandezdiaz Aug 11, 2025
0aafc52
Updating branch
ifernandezdiaz Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Comment on lines +288 to +304
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO and commented code still needed? If so, can we associate it to a ticket so it isn't forgotten?

- name: Generate beta Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO needs to be addressed.

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:
Expand Down
84 changes: 79 additions & 5 deletions .github/workflows/test-device.yml
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checks: write is only needed to create or update status checks. Neither are happening directly in the job, that I can see. I don't think this permission is needed.

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 changes: 32 additions & 0 deletions .sauce/config.yml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
Comment on lines +259 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove these as implementation dependencies?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, the build fails if I remove those dependencies ๐Ÿค”

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think he want's them to be an androidTestImplementation to avoid having these test dependencies in the non-test builds

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but those deps are already present as androidTestImplementation in lines 302-306.
Apart from that, we need those deps in non-test builds since we aim to test the release build on real devices

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿค” We do not want to ship with unnecessary dependencies like this. Is there a specific error you are seeing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a way to build the required testApp without those deps ๐Ÿซค

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need a new variant for this.

ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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.** { *; }
Copy link
Collaborator

@david-livefront david-livefront Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Are we obfuscating test code?

5 changes: 5 additions & 0 deletions app/src/androidTest/assets/TestData.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"baseUrl": "_",
"email": "_",
"password": "_"
}
10 changes: 10 additions & 0 deletions app/src/androidTest/kotlin/com/x8bit/bitwarden/data/TestData.kt
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading