Skip to content

Commit a6fd61c

Browse files
authored
Add UI Tests (#2847)
* Add ECA, Beacon and MultiUser Tests. Make efficient use of multiple UI Test users. Fix issues with Token Migration of non-current user. * Add AuthFlowTester readme. Use performSemanticAction or more reliable element clicks. Change timeout values based on if running in CI. * Add APK and test result archiving. * Fix test result reporting. * Improve test stability. * Remove debug logline. * Update bad token length assumption. * Stop batching UI tests and use num-flaky-test-attempts=1 to help with rare user collision issues. Logout all users after each test. Add descriptive errors to help triage test failures. * Fix flaky test result reporting. * Merge original and retry test results so they are interpreted correctly by the test reporter.
1 parent 5f1304b commit a6fd61c

22 files changed

Lines changed: 1902 additions & 305 deletions

File tree

.github/workflows/reusable-lib-workflow.yaml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,38 @@ jobs:
173173
# Sharded runs produce test_results_merged.xml at top level
174174
gsutil cp "${BUCKET_PATH}/*test_results_merged.xml" firebase_results/api_${LEVEL}_test_result.xml
175175
else
176-
gsutil cp "${BUCKET_PATH}/*/test_result_1.xml" firebase_results/api_${LEVEL}_test_result.xml
176+
# Pass 1: copy original (non-rerun) results
177+
for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep -v "rerun"); do
178+
gsutil cp "${RESULT_FILE}" "firebase_results/api_${LEVEL}_test_result.xml"
179+
done
180+
# Pass 2: merge rerun testcases into originals so check_retries detects flaky tests
181+
for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep "rerun"); do
182+
RERUN_TMP="firebase_results/api_${LEVEL}_rerun_tmp.xml"
183+
ORIG_FILE="firebase_results/api_${LEVEL}_test_result.xml"
184+
gsutil cp "${RESULT_FILE}" "${RERUN_TMP}"
185+
python3 - "${ORIG_FILE}" "${RERUN_TMP}" "${ORIG_FILE}" << 'PYEOF'
186+
import sys, xml.etree.ElementTree as ET
187+
orig = ET.parse(sys.argv[1])
188+
rerun = ET.parse(sys.argv[2])
189+
def suite(t):
190+
r = t.getroot()
191+
return r if r.tag == 'testsuite' else r.find('testsuite')
192+
os_el, rs_el = suite(orig), suite(rerun)
193+
failed_keys = set()
194+
for tc in os_el.findall('testcase'):
195+
if tc.find('failure') is not None or tc.find('error') is not None:
196+
failed_keys.add(f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}")
197+
added = 0
198+
for tc in rs_el.findall('testcase'):
199+
if f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}" in failed_keys:
200+
os_el.append(tc)
201+
added += 1
202+
os_el.set('tests', str(int(os_el.get('tests','0')) + added))
203+
with open(sys.argv[3], 'w') as f:
204+
f.write(ET.tostring(orig.getroot(), encoding='unicode'))
205+
PYEOF
206+
rm "${RERUN_TMP}"
207+
done
177208
fi
178209
179210
# Copy all shard data for code coverage (only needed for one level)
@@ -198,6 +229,12 @@ jobs:
198229
include_empty_in_summary: false
199230
simplified_summary: true
200231
report_paths: 'firebase_results/**.xml'
232+
- name: Archive Test Results
233+
uses: actions/upload-artifact@v4
234+
if: success() || failure()
235+
with:
236+
name: test-results-${{ inputs.lib }}
237+
path: 'firebase_results/**.xml'
201238
- name: Convert Code Coverage
202239
if: success() || failure()
203240
run: ./gradlew libs:${{ inputs.lib }}:convertCodeCoverage

.github/workflows/reusable-ui-workflow.yaml

Lines changed: 153 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ jobs:
4141
gradle-version: "8.14.3"
4242
add-job-summary: on-failure
4343
add-job-summary-as-pr-comment: on-failure
44-
- name: Build for Testing
44+
- name: Build App for Testing
4545
if: success() || failure()
4646
run: |
4747
./gradlew native:NativeSampleApps:AuthFlowTester:assembleDebug
48-
- name: Build Tests
48+
- name: Build UI Tests
4949
run: |
5050
./gradlew native:NativeSampleApps:AuthFlowTester:assembleAndroidTest
5151
- uses: 'google-github-actions/auth@v2'
@@ -54,84 +54,163 @@ jobs:
5454
credentials_json: '${{ secrets.GCLOUD_SERVICE_KEY }}'
5555
- uses: 'google-github-actions/setup-gcloud@v2'
5656
if: success() || failure()
57-
- name: Run Tests
57+
- name: Run PR Tests
5858
continue-on-error: true
59-
if: success() || failure()
59+
if: ${{ inputs.is_pr }}
6060
env:
6161
# Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide
6262
PR_API_VERSION: "35"
63+
run: |
64+
GCLOUD_RESULTS_DIR=authflowtester-pr-build-${{github.run_number}}
65+
66+
PR_TESTS="class com.salesforce.samples.authflowtester.BootConfigLoginTests#testCAOpaque_DefaultScopes_WebServerFlow, \
67+
class com.salesforce.samples.authflowtester.ECALoginTests#testECAOpaque_DefaultScopes, \
68+
class com.salesforce.samples.authflowtester.ECALoginTests#testECAJwt_AllScopes, \
69+
class com.salesforce.samples.authflowtester.TokenMigrationTest#testMigrate_ECA_AddMoreScopes, \
70+
class com.salesforce.samples.authflowtester.MultiUserLoginTests#testSameApp_SameScopes_uniqueTokens"
71+
72+
gcloud firebase test android run \
73+
--project mobile-apps-firebase-test \
74+
--type instrumentation \
75+
--use-orchestrator \
76+
--environment-variables clearPackageData=true \
77+
--app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \
78+
--test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \
79+
--device model=MediumPhone.arm,version="${PR_API_VERSION}",locale=en,orientation=portrait \
80+
--directories-to-pull=/sdcard \
81+
--results-dir="${GCLOUD_RESULTS_DIR}" \
82+
--results-history-name=AuthFlowTester \
83+
--no-performance-metrics \
84+
--test-targets="${PR_TESTS}" \
85+
--timeout=10m \
86+
--num-flaky-test-attempts=1
87+
- name: Run All Single User Tests
88+
continue-on-error: true
89+
if: ${{ ! inputs.is_pr }}
90+
env:
6391
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
64-
IS_PR: ${{ inputs.is_pr }}
6592
run: |
66-
LEVELS_TO_TEST=$FULL_API_RANGE
67-
RETRIES=0
68-
TEST_TARGETS=""
69-
70-
if $IS_PR ; then
71-
LEVELS_TO_TEST=$PR_API_VERSION
72-
RETRIES=1
73-
# Run only a handful of smoke tests.
74-
TEST_TARGETS="--test-targets \"class com.salesforce.samples.authflowtester.LoginTest#testBasicLogin\""
75-
TEST_TARGETS+=",\"class com.salesforce.samples.authflowtester.TokenMigrationTest#testMigrate_ECA_AddMoreScopes\""
76-
fi
77-
78-
mkdir firebase_results
79-
for LEVEL in $LEVELS_TO_TEST
80-
do
81-
GCLOUD_RESULTS_DIR=authflowtester-api-${LEVEL}-build-${{github.run_number}}
82-
83-
eval gcloud firebase test android run \
84-
--project mobile-apps-firebase-test \
85-
--type instrumentation \
86-
--use-orchestrator \
87-
--environment-variables clearPackageData=true \
88-
--app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \
89-
--test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \
90-
--device model=MediumPhone.arm,version=${LEVEL},locale=en,orientation=portrait \
91-
--directories-to-pull=/sdcard \
92-
--results-dir=${GCLOUD_RESULTS_DIR} \
93-
--results-history-name=AuthFlowTester \
94-
--timeout=10m --no-performance-metrics \
95-
$TEST_TARGETS \
96-
--num-flaky-test-attempts=${RETRIES} || true
97-
done
93+
GCLOUD_RESULTS_DIR=authflowtester-single-user-build-${{github.run_number}}
94+
DEVICE_ARGS=()
95+
for LEVEL in $FULL_API_RANGE; do
96+
DEVICE_ARGS+=(--device "model=MediumPhone.arm,version=${LEVEL},locale=en,orientation=portrait")
97+
done
98+
99+
gcloud firebase test android run \
100+
--project mobile-apps-firebase-test \
101+
--type instrumentation \
102+
--use-orchestrator \
103+
--environment-variables clearPackageData=true \
104+
--app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \
105+
--test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \
106+
--test-targets "notClass com.salesforce.samples.authflowtester.MultiUserLoginTests" \
107+
"${DEVICE_ARGS[@]}" \
108+
--directories-to-pull=/sdcard \
109+
--results-dir="${GCLOUD_RESULTS_DIR}" \
110+
--results-history-name=AuthFlowTester \
111+
--no-performance-metrics \
112+
--num-flaky-test-attempts=1 \
113+
--timeout=30m || true
114+
- name: Run All Multi User Tests
115+
continue-on-error: true
116+
if: ${{ ! inputs.is_pr }}
117+
env:
118+
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
119+
run: |
120+
GCLOUD_RESULTS_DIR=authflowtester-multi-user-build-${{github.run_number}}
121+
DEVICE_ARGS=()
122+
for LEVEL in $FULL_API_RANGE; do
123+
DEVICE_ARGS+=(--device "model=MediumPhone.arm,version=${LEVEL},locale=en,orientation=portrait")
124+
done
125+
126+
gcloud firebase test android run \
127+
--project mobile-apps-firebase-test \
128+
--type instrumentation \
129+
--use-orchestrator \
130+
--environment-variables clearPackageData=true \
131+
--app "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk" \
132+
--test "native/NativeSampleApps/AuthFlowTester/build/outputs/apk/androidTest/debug/AuthFlowTester-debug-androidTest.apk" \
133+
--test-targets "class com.salesforce.samples.authflowtester.MultiUserLoginTests" \
134+
"${DEVICE_ARGS[@]}" \
135+
--directories-to-pull=/sdcard \
136+
--results-dir="${GCLOUD_RESULTS_DIR}" \
137+
--results-history-name=AuthFlowTester \
138+
--no-performance-metrics \
139+
--num-flaky-test-attempts=1 \
140+
--timeout=15m || true
98141
- name: Copy Test Results
99142
continue-on-error: true
100143
if: success() || failure()
101144
env:
102-
# Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide
103-
PR_API_VERSION: "35"
104-
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
105145
IS_PR: ${{ inputs.is_pr }}
106146
run: |
107-
LEVELS_TO_TEST=$FULL_API_RANGE
147+
mkdir -p firebase_results
148+
BUCKET="gs://test-lab-w87i9sz6q175u-kwp8ium6js0zw"
108149
109-
if $IS_PR ; then
110-
LEVELS_TO_TEST=$PR_API_VERSION
111-
fi
150+
copy_results_by_api_level() {
151+
local BUCKET_PATH=$1
152+
local OUTPUT_PREFIX=$2
112153
113-
for LEVEL in $LEVELS_TO_TEST
114-
do
115-
GCLOUD_RESULTS_DIR=authflowtester-api-${LEVEL}-build-${{github.run_number}}
116-
BUCKET_PATH="gs://test-lab-w87i9sz6q175u-kwp8ium6js0zw/${GCLOUD_RESULTS_DIR}"
117-
118-
gsutil ls ${BUCKET_PATH} > /dev/null 2>&1
119-
if [ $? == 0 ] ; then
120-
# Copy XML file for test reporting
121-
if gsutil ls "${BUCKET_PATH}/*test_results_merged.xml" > /dev/null 2>&1; then
122-
# Sharded runs produce test_results_merged.xml at top level
123-
gsutil cp "${BUCKET_PATH}/*test_results_merged.xml" firebase_results/api_${LEVEL}_test_result.xml
124-
else
125-
gsutil cp "${BUCKET_PATH}/*/test_result_1.xml" firebase_results/api_${LEVEL}_test_result.xml
126-
fi
127-
fi
154+
# Pass 1: copy original (non-rerun) results
155+
for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep -v "rerun"); do
156+
DEVICE_DIR=$(echo "${RESULT_FILE}" | sed 's|.*/\([^/]*\)/test_result_1.xml|\1|')
157+
API_LEVEL=$(echo "${DEVICE_DIR}" | sed 's/.*-\([0-9]*\)-.*/\1/')
158+
gsutil cp "${RESULT_FILE}" "firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_test_result.xml"
159+
done
160+
# Pass 2: merge rerun testcases into originals so check_retries detects flaky tests
161+
for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep "rerun"); do
162+
DEVICE_DIR=$(echo "${RESULT_FILE}" | sed 's|.*/\([^/]*\)/test_result_1.xml|\1|')
163+
API_LEVEL=$(echo "${DEVICE_DIR}" | sed 's/.*-\([0-9]*\)-.*/\1/')
164+
RERUN_TMP="firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_rerun_tmp.xml"
165+
ORIG_FILE="firebase_results/${OUTPUT_PREFIX}_api_${API_LEVEL}_test_result.xml"
166+
gsutil cp "${RESULT_FILE}" "${RERUN_TMP}"
167+
python3 - "${ORIG_FILE}" "${RERUN_TMP}" "${ORIG_FILE}" << 'PYEOF'
168+
import sys, xml.etree.ElementTree as ET
169+
orig = ET.parse(sys.argv[1])
170+
rerun = ET.parse(sys.argv[2])
171+
def suite(t):
172+
r = t.getroot()
173+
return r if r.tag == 'testsuite' else r.find('testsuite')
174+
os_el, rs_el = suite(orig), suite(rerun)
175+
failed_keys = set()
176+
for tc in os_el.findall('testcase'):
177+
if tc.find('failure') is not None or tc.find('error') is not None:
178+
failed_keys.add(f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}")
179+
added = 0
180+
for tc in rs_el.findall('testcase'):
181+
if f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}" in failed_keys:
182+
os_el.append(tc)
183+
added += 1
184+
os_el.set('tests', str(int(os_el.get('tests','0')) + added))
185+
with open(sys.argv[3], 'w') as f:
186+
f.write(ET.tostring(orig.getroot(), encoding='unicode'))
187+
PYEOF
188+
rm "${RERUN_TMP}"
128189
done
190+
}
191+
192+
if $IS_PR ; then
193+
BUCKET_PATH="${BUCKET}/authflowtester-pr-build-${{github.run_number}}"
194+
if gsutil ls "${BUCKET_PATH}" > /dev/null 2>&1; then
195+
copy_results_by_api_level "${BUCKET_PATH}" "pr"
196+
fi
197+
else
198+
SINGLE_PATH="${BUCKET}/authflowtester-single-user-build-${{github.run_number}}"
199+
if gsutil ls "${SINGLE_PATH}" > /dev/null 2>&1; then
200+
copy_results_by_api_level "${SINGLE_PATH}" "single-user"
201+
fi
202+
203+
MULTI_PATH="${BUCKET}/authflowtester-multi-user-build-${{github.run_number}}"
204+
if gsutil ls "${MULTI_PATH}" > /dev/null 2>&1; then
205+
copy_results_by_api_level "${MULTI_PATH}" "multi-user"
206+
fi
207+
fi
129208
- name: Test Report
130209
uses: mikepenz/action-junit-report@v6
131210
if: success() || failure()
132211
with:
133-
check_name: ${{ inputs.lib }} Test Results
134-
job_name: ${{ inputs.lib }} Test Results
212+
check_name: AuthFlowTester Test Results
213+
job_name: AuthFlowTester Test Results
135214
require_tests: true
136215
check_retries: true
137216
flaky_summary: true
@@ -140,4 +219,16 @@ jobs:
140219
include_passed: true
141220
include_empty_in_summary: false
142221
simplified_summary: true
143-
report_paths: 'firebase_results/**.xml'
222+
report_paths: 'firebase_results/**.xml'
223+
- name: Archive APK
224+
if: success() || failure()
225+
uses: actions/upload-artifact@v4
226+
with:
227+
name: AuthFlowTester-debug-${{ github.run_number }}
228+
path: native/NativeSampleApps/AuthFlowTester/build/outputs/apk/debug/AuthFlowTester-debug.apk
229+
- name: Archive Test Results
230+
uses: actions/upload-artifact@v4
231+
if: success() || failure()
232+
with:
233+
name: ui-test-results
234+
path: 'firebase_results/**.xml'

0 commit comments

Comments
 (0)