Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ jobs:
matrix:
lib: [SalesforceReact]
uses: ./.github/workflows/reusable-workflow.yaml
with:
lib: ${{ matrix.lib }}
secrets: inherit
android-nightly-UI-Tests:
if: success() || failure()
needs: [android-nightly-React]
strategy:
fail-fast: false
matrix:
lib: [App]
uses: ./.github/workflows/reusable-workflow.yaml
with:
lib: ${{ matrix.lib }}
secrets: inherit
140 changes: 140 additions & 0 deletions .github/workflows/reusable-ui-workflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
on:
workflow_call:
inputs:
is_pr:
type: boolean
default: false

jobs:
test-android:
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/.github/DangerFiles/Gemfile
steps:
- uses: actions/checkout@v4
if: ${{ inputs.is_pr }}
with:
# We need a sufficient depth or Danger will occasionally run into issues checking which files were modified.
fetch-depth: 100
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/checkout@v4
if: ${{ ! inputs.is_pr }}
with:
ref: ${{ github.head_ref }}
- name: Install Dependencies
env:
TEST_CREDENTIALS: ${{ secrets.TEST_CREDENTIALS }}
run: |
./install.sh
echo $TEST_CREDENTIALS > ./shared/test/test_credentials.json
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- uses: gradle/actions/setup-gradle@v4
with:
# This is the actual Gradle version (not AGP), which can be found in the gradle-wrapper.properties file.
gradle-version: "8.14.3"
add-job-summary: on-failure
add-job-summary-as-pr-comment: on-failure
- name: Build for Testing
if: success() || failure()
run: |
./gradlew native:NativeSampleApps:AuthFlowTester:assembleDebug
- name: Build Tests for PR
if: ${{ inputs.is_pr }}
run: |
./gradlew native:NativeSampleApps:AuthFlowTester:assembleAndroidTest --tests "com.salesforce.samples.authflowtester.loginTest"
- name: Build Tests for Nightly
if: ${{ !inputs.is_pr }}
run: |
./gradlew native:NativeSampleApps:AuthFlowTester:assembleAndroidTest
- uses: 'google-github-actions/auth@v2'
if: success() || failure()
with:
credentials_json: '${{ secrets.GCLOUD_SERVICE_KEY }}'
- uses: 'google-github-actions/setup-gcloud@v2'
if: success() || failure()
- name: Run Tests
continue-on-error: true
if: success() || failure()
env:
# Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide
PR_API_VERSION: "35"
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
IS_PR: ${{ inputs.is_pr }}
run: |
LEVELS_TO_TEST=$FULL_API_RANGE
RETRIES=0

if $IS_PR ; then
LEVELS_TO_TEST=$PR_API_VERSION
RETRIES=1
fi

mkdir firebase_results
for LEVEL in $LEVELS_TO_TEST
do
GCLOUD_RESULTS_DIR=authflowtester-api-${LEVEL}-build-${{github.run_number}}

eval gcloud beta firebase test android run \
--project mobile-apps-firebase-test \
--type instrumentation \
--app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \
--test="native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \
--device model=MediumPhone.arm,version=${LEVEL},locale=en,orientation=portrait \
--environment-variables \
--directories-to-pull=/sdcard \
--results-dir=${GCLOUD_RESULTS_DIR} \
--results-history-name=AuthFlowTester \
--timeout=30m --no-performance-metrics \
--num-flaky-test-attempts=${RETRIES} || true
done
- name: Copy Test Results
continue-on-error: true
if: success() || failure()
env:
# Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide
PR_API_VERSION: "35"
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
IS_PR: ${{ inputs.is_pr }}
run: |
LEVELS_TO_TEST=$FULL_API_RANGE

if $IS_PR ; then
LEVELS_TO_TEST=$PR_API_VERSION
fi

for LEVEL in $LEVELS_TO_TEST
do
GCLOUD_RESULTS_DIR=authflowtester-api-${LEVEL}-build-${{github.run_number}}
BUCKET_PATH="gs://test-lab-w87i9sz6q175u-kwp8ium6js0zw/${GCLOUD_RESULTS_DIR}"

gsutil ls ${BUCKET_PATH} > /dev/null 2>&1
if [ $? == 0 ] ; then
# Copy XML file for test reporting
if gsutil ls "${BUCKET_PATH}/*test_results_merged.xml" > /dev/null 2>&1; then
# Sharded runs produce test_results_merged.xml at top level
gsutil cp "${BUCKET_PATH}/*test_results_merged.xml" firebase_results/api_${LEVEL}_test_result.xml
else
gsutil cp "${BUCKET_PATH}/*/test_result_1.xml" firebase_results/api_${LEVEL}_test_result.xml
fi
fi
done
- name: Test Report
uses: mikepenz/action-junit-report@v5
if: success() || failure()
with:
check_name: ${{ inputs.lib }} Test Results
job_name: ${{ inputs.lib }} Test Results
require_tests: true
check_retries: true
flaky_summary: true
fail_on_failure: true
group_reports: false
include_passed: true
include_empty_in_summary: false
simplified_summary: true
report_paths: 'firebase_results/**.xml'
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "external/shared"]
path = external/shared
url = https://github.com/forcedotcom/SalesforceMobileSDK-Shared.git
[submodule "external/SalesforceMobileSDK-UITests"]
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not a big fan of having the main repo having a test repo as submodule.
Right now it's only used for tests for AuthFlowTester but still.
Could we inverse the dependency? Have shared test code live in the main repo (next to the UI code it is for) and have the UITests repo pull it from there?

Copy link
Contributor Author

@brandonpage brandonpage Jan 16, 2026

Choose a reason for hiding this comment

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

I guess I could try it, but that does not make sense in my head. The UITests repo has a single, narrow, purpose and needing to pull in SalesforceMobileSDK-Android, SalesforceMobileSDK-iOS, SalesforceMobileSDK-iOS-Hybrid and SalesforceMobileSDK-React seems backwards. All of the shared UITest infrastructure should live in 1 place (not 4) IMO.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we can figure that out later.

path = external/SalesforceMobileSDK-UITests
url = https://github.com/forcedotcom/SalesforceMobileSDK-UITests
1 change: 1 addition & 0 deletions external/SalesforceMobileSDK-UITests
1 change: 1 addition & 0 deletions libs/SalesforceSDK/res/values/sf__strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@

<!-- Developer Support -->
<string name="sf__dev_support_title">Mobile SDK Developer Support</string>
<string name="sf__dev_support_title_menu_item">Developer Support</string>
<string name="sf__notifications_local_show_dev_support_content">Show Salesforce Mobile SDK developer support</string>
<string name="sf__notifications_local_show_dev_support_text">Tap to display Salesforce Mobile SDK developer support in the active app.</string>
<string name="sf__notifications_local_show_dev_support_title">Salesforce Mobile Developer Support</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class ScopeParser {
return ScopeParser(scopeString)
}

/**
* String extension to convert to [ScopeParser].
*/
fun String?.toScopeParser(): ScopeParser = ScopeParser(scopeString = this)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

One of only a few SDK changes. This (and Array<String>?.toScopeParameter() below) seemed like they maybe convenient for customers so I created them as public API but I could move them to the sample app if we want.


/**
* Computes the scope parameter from an array of scopes.
*
Expand All @@ -74,6 +79,18 @@ class ScopeParser {
scopesSet.add(REFRESH_TOKEN)
return scopesSet.joinToString(" ")
}

/**
* Computes the scope parameter from an array of scopes.
*
* Behavior:
* - If {@code scopes} is null or empty, returns an empty string. This indicates that all
* scopes assigned to the connected app / external client app will be requested by default
* (no explicit scope parameter is sent).
* - If {@code scopes} is non-empty, ensures {@code refresh_token} is present in the set and
* returns a space-delimited string of unique, sorted scopes.
*/
fun Array<String>?.toScopeParameter(): String = computeScopeParameter(this)
}

private val _scopes: MutableSet<String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import com.salesforce.androidsdk.R.string.sf__server_url_delete
import com.salesforce.androidsdk.config.LoginServerManager.LoginServer
import com.salesforce.androidsdk.ui.theme.sfDarkColors
import com.salesforce.androidsdk.ui.theme.sfLightColors
import com.salesforce.androidsdk.util.test.ExcludeFromJacocoGeneratedReport
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -244,6 +245,7 @@ fun LoginServerListItem(
}
}

@ExcludeFromJacocoGeneratedReport
@Preview("Default Server", showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, backgroundColor = 0xFF181818)
@Composable
Expand All @@ -258,6 +260,7 @@ private fun DefaultServerPreview() {
}
}

@ExcludeFromJacocoGeneratedReport
@Preview("Very Long Default Server", showBackground = true)
@Composable
private fun LongDefaultServerPreview() {
Expand All @@ -275,6 +278,7 @@ private fun LongDefaultServerPreview() {
}
}

@ExcludeFromJacocoGeneratedReport
@Preview("Custom Server", showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, backgroundColor = 0xFF181818)
@Composable
Expand All @@ -289,6 +293,7 @@ private fun CustomServerPreview() {
}
}

@ExcludeFromJacocoGeneratedReport
@Preview("Deleting", showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, backgroundColor = 0xFF181818)
@Composable
Expand All @@ -304,6 +309,7 @@ private fun DeletingLoginServer() {
}
}

@ExcludeFromJacocoGeneratedReport
@Preview("Very Long Custom Server", showBackground = true)
@Composable
private fun LongServerPreview() {
Expand All @@ -321,6 +327,7 @@ private fun LongServerPreview() {
}
}

@ExcludeFromJacocoGeneratedReport
@Preview("Very Long Custom Server Deleting", showBackground = true)
@Composable
private fun LongServerDeletingPreview() {
Expand Down
Loading