mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Compare commits
52 Commits
v2025.11.0
...
QA-1126b/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aafc52231 | ||
|
|
9bf3d1ed0d | ||
|
|
da9b60f5ed | ||
|
|
15b5b86b34 | ||
|
|
12edccc4b3 | ||
|
|
bb8dda4442 | ||
|
|
f803752861 | ||
|
|
ab481d29eb | ||
|
|
b74d3b1caa | ||
|
|
a6efb311fa | ||
|
|
b1a74c6fae | ||
|
|
b6e90e487b | ||
|
|
be325eb43c | ||
|
|
c4ebde786c | ||
|
|
7a985557aa | ||
|
|
62b9088a5a | ||
|
|
6be753b4ed | ||
|
|
bbd7e7c00d | ||
|
|
909b0d03cb | ||
|
|
7dc5b99342 | ||
|
|
47a9117590 | ||
|
|
9474133622 | ||
|
|
cab4461bcf | ||
|
|
d3d02f4a10 | ||
|
|
85f0bd920c | ||
|
|
7bedb1f4fd | ||
|
|
e20c8149e8 | ||
|
|
a5721f3e59 | ||
|
|
9a724d805b | ||
|
|
c6f8fe2431 | ||
|
|
a6bd9b6dda | ||
|
|
e9afd92912 | ||
|
|
5fc3c0227e | ||
|
|
22644ab21c | ||
|
|
6b2ac29655 | ||
|
|
4b2f0da0ae | ||
|
|
b8172f9c4b | ||
|
|
a371efbd6b | ||
|
|
26cb4cedfe | ||
|
|
b0fb07110d | ||
|
|
a44a8dc6d4 | ||
|
|
58c98fa297 | ||
|
|
2856545888 | ||
|
|
a2ed706625 | ||
|
|
772245cfc5 | ||
|
|
6181378f28 | ||
|
|
2af019e555 | ||
|
|
047b762912 | ||
|
|
a6ef2ea78d | ||
|
|
5b012e1d23 | ||
|
|
ed5b752717 | ||
|
|
815c73944f |
64
.github/workflows/build.yml
vendored
64
.github/workflows/build.yml
vendored
@@ -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:
|
||||
|
||||
84
.github/workflows/test-device.yml
vendored
84
.github/workflows/test-device.yml
vendored
@@ -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
32
.sauce/config.yml
Normal 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
|
||||
21
README.md
21
README.md
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
20
app/proguard-rules.pro
vendored
20
app/proguard-rules.pro
vendored
@@ -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.** { *; }
|
||||
|
||||
5
app/src/androidTest/assets/TestData.json
Normal file
5
app/src/androidTest/assets/TestData.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"baseUrl": "_",
|
||||
"email": "_",
|
||||
"password": "_"
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user