Skip to content

Commit 61c5cf4

Browse files
authored
StripeCryptoOnramp: Full Flow E2E Test (#12070)
1 parent cb430a5 commit 61c5cf4

File tree

6 files changed

+228
-39
lines changed

6 files changed

+228
-39
lines changed

bitrise.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ stages:
4242
- run-connections-e2e-token-tests-on-push: { }
4343
- run-connections-e2e-data-tests-on-push: { }
4444
- run-paymentsheet-end-to-end-tests: { }
45+
- run-crypto-onramp-example-e2e-tests: { }
4546
- check-dependencies: { }
4647
stage-run-connect-e2e-tests:
4748
workflows:
@@ -331,6 +332,25 @@ workflows:
331332
timeout: 2400
332333
inputs:
333334
- content: python3 scripts/browserstack.py --test --apk paymentsheet-example/build/outputs/apk/base/debug/paymentsheet-example-base-debug.apk --espresso paymentsheet-example/build/outputs/apk/androidTest/base/debug/paymentsheet-example-base-debug-androidTest.apk --num-retries 2
335+
run-crypto-onramp-example-e2e-tests:
336+
before_run:
337+
- prepare_all
338+
- start_emulator
339+
after_run:
340+
- conclude_all
341+
steps:
342+
- script@1:
343+
timeout: 1200
344+
inputs:
345+
- content: ./gradlew :crypto-onramp-example:assembleDebug :crypto-onramp-example:assembleDebugAndroidTest
346+
- wait-for-android-emulator@1:
347+
inputs:
348+
- boot_timeout: 600
349+
- script@1:
350+
title: Run Crypto Onramp Example E2E Tests
351+
timeout: 1200
352+
inputs:
353+
- content: ./gradlew :crypto-onramp-example:connectedDebugAndroidTest
334354
check-dependencies:
335355
envs:
336356
- INCLUDE_DEPENDENCIES_ON_FAILURE: true

crypto-onramp-example/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ dependencies {
3838

3939
testImplementation testLibs.junit
4040

41+
androidTestImplementation project(':payments-core-testing')
4142
androidTestImplementation testLibs.androidx.junit
4243
androidTestImplementation testLibs.androidx.testRunner
4344
androidTestImplementation testLibs.androidx.uiAutomator
45+
androidTestImplementation testLibs.androidx.archCore
46+
androidTestImplementation testLibs.androidx.composeUi
47+
androidTestImplementation testLibs.androidx.core
48+
androidTestImplementation testLibs.androidx.coreKtx
49+
androidTestImplementation testLibs.androidx.testRules
50+
androidTestImplementation testLibs.androidx.truth
4451
}

crypto-onramp-example/src/androidTest/java/com/stripe/android/crypto/onramp/example/ExampleInstrumentedTest.kt

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.stripe.android.crypto.onramp.example
2+
3+
import android.content.Context
4+
import androidx.compose.ui.test.hasTestTag
5+
import androidx.compose.ui.test.hasText
6+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
7+
import androidx.compose.ui.test.onNodeWithTag
8+
import androidx.compose.ui.test.performTextInput
9+
import androidx.compose.ui.test.ExperimentalTestApi
10+
import androidx.compose.ui.test.SemanticsNodeInteraction
11+
import androidx.compose.ui.test.performClick
12+
import androidx.compose.ui.test.performScrollTo
13+
import androidx.compose.ui.test.hasSetTextAction
14+
import androidx.compose.ui.test.SemanticsMatcher
15+
import androidx.test.core.app.ApplicationProvider
16+
import androidx.test.ext.junit.runners.AndroidJUnit4
17+
import com.stripe.android.core.utils.FeatureFlags
18+
import com.stripe.android.testing.FeatureFlagTestRule
19+
import org.junit.Before
20+
import org.junit.Rule
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import kotlin.time.Duration
24+
import kotlin.time.Duration.Companion.seconds
25+
26+
@RunWith(AndroidJUnit4::class)
27+
class OnrampFlowTest {
28+
@get:Rule
29+
internal val composeRule = createAndroidComposeRule<OnrampActivity>()
30+
31+
@get:Rule
32+
internal val attestationFeatureFlagTestRule = FeatureFlagTestRule(
33+
featureFlag = FeatureFlags.nativeLinkAttestationEnabled,
34+
isEnabled = false
35+
)
36+
37+
private val defaultTimeout: Duration = 15.seconds
38+
39+
@Before
40+
fun clearPrefs() {
41+
val context = ApplicationProvider.getApplicationContext<Context>()
42+
43+
context.getSharedPreferences(ONRAMP_PREFS_NAME, Context.MODE_PRIVATE)
44+
.edit()
45+
.clear()
46+
.commit()
47+
}
48+
49+
@OptIn(ExperimentalTestApi::class)
50+
@Test
51+
fun testCheckoutFlow() {
52+
waitForTag(LOGIN_EMAIL_TAG)
53+
54+
// Enter test login credentials previously registered with the demo backend.
55+
composeRule.onNodeWithTag(LOGIN_EMAIL_TAG)
56+
.performTextInput("[email protected]")
57+
58+
composeRule.onNodeWithTag(LOGIN_PASSWORD_TAG)
59+
.performTextInput("testing1234")
60+
61+
performClickOnNode(LOGIN_LOGIN_BUTTON_TAG)
62+
performClickOnNode(AUTHENTICATE_BUTTON_TAG)
63+
64+
waitForTag("OTP-0")
65+
66+
for (i in 0..5) {
67+
composeRule
68+
.onNodeWithTag("OTP-$i")
69+
.slowType("0")
70+
}
71+
72+
performClickOnNode(REGISTER_WALLET_BUTTON_TAG)
73+
performClickOnNode(COLLECT_CARD_BUTTON_TAG)
74+
75+
// Optionally wait for a CVV field and input if present
76+
val cvvMatcher = hasSetTextAction()
77+
if (waitForOptionalNode(cvvMatcher, timeoutMs = 5.seconds.inWholeMilliseconds)) {
78+
val cvvNode = try {
79+
composeRule.onNode(cvvMatcher)
80+
} catch (_: Throwable) {
81+
composeRule.onNode(cvvMatcher, useUnmergedTree = true)
82+
}
83+
cvvNode.performScrollTo()
84+
cvvNode.performTextInput("555")
85+
}
86+
87+
performClickOnNode("PrimaryButtonTag")
88+
performClickOnNode(CREATE_CRYPTO_TOKEN_BUTTON_TAG)
89+
performClickOnNode(CREATE_SESSION_BUTTON_TAG)
90+
performClickOnNode(CHECKOUT_BUTTON_TAG, timeoutMs = 30.seconds.inWholeMilliseconds)
91+
92+
composeRule.waitUntilExactlyOneExists(
93+
hasText("Checkout completed successfully!"),
94+
timeoutMillis = 30.seconds.inWholeMilliseconds
95+
)
96+
}
97+
98+
private fun SemanticsNodeInteraction.slowType(
99+
text: String,
100+
delayMs: Long = 200
101+
) {
102+
text.forEach { char ->
103+
this.performTextInput(char.toString())
104+
Thread.sleep(delayMs)
105+
}
106+
}
107+
108+
@OptIn(ExperimentalTestApi::class)
109+
private fun waitForSnackbarToHide(timeoutMs: Long = defaultTimeout.inWholeMilliseconds) {
110+
composeRule.waitUntilDoesNotExist(hasTestTag(SNACKBAR_TAG), timeoutMillis = timeoutMs)
111+
}
112+
113+
@OptIn(ExperimentalTestApi::class)
114+
private fun waitForTag(tag: String, timeoutMs: Long = defaultTimeout.inWholeMilliseconds) {
115+
composeRule.waitUntilExactlyOneExists(
116+
hasTestTag(tag),
117+
timeoutMillis = timeoutMs
118+
)
119+
}
120+
121+
private fun waitForOptionalNode(
122+
matcher: SemanticsMatcher,
123+
timeoutMs: Long = defaultTimeout.inWholeMilliseconds
124+
): Boolean {
125+
return runCatching {
126+
composeRule.waitUntil(timeoutMs) {
127+
composeRule.onAllNodes(matcher).fetchSemanticsNodes().isNotEmpty()
128+
}
129+
true
130+
}.getOrElse { false }
131+
}
132+
133+
private fun performClickOnNode(tag: String, timeoutMs: Long = defaultTimeout.inWholeMilliseconds) {
134+
waitForTag(tag = tag, timeoutMs = timeoutMs)
135+
waitForSnackbarToHide()
136+
137+
val node = composeRule.onNodeWithTag(tag)
138+
runCatching { node.performScrollTo() }
139+
node.performClick()
140+
}
141+
}

0 commit comments

Comments
 (0)