Skip to content

Commit 18caae2

Browse files
committed
Add native Android checkout e2e flow
1 parent 2988409 commit 18caae2

8 files changed

Lines changed: 468 additions & 9 deletions

File tree

e2e/README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ terminal.
5252
| ------------------ | ------------------------------- | ------------------ |
5353
| React Native, iOS | `platforms/react-native/` | `pnpm e2e:ios` |
5454
| Swift, iOS | `platforms/swift/` | `./Scripts/e2e_maestro_ios` |
55-
| Android (native) | TBD | TBD |
55+
| Android (native) | `platforms/android/` | `./scripts/e2e_maestro_android` |
5656
| RN, Android | `platforms/react-native/` | `pnpm e2e:android` |
5757

5858
Maestro itself is a system CLI, not an npm dependency. Install once with:
@@ -61,6 +61,31 @@ Maestro itself is a system CLI, not an npm dependency. Install once with:
6161
curl -fsSL "https://get.maestro.mobile.dev" | bash
6262
```
6363

64+
To pin the native Android runner to a specific emulator, set
65+
`MAESTRO_ANDROID_UDID`:
66+
67+
```
68+
MAESTRO_ANDROID_UDID=emulator-5556 ./scripts/e2e_maestro_android
69+
```
70+
71+
If local Android runs fail before `launchApp` with `deviceInfo`,
72+
`io.grpc.StatusRuntimeException: UNAVAILABLE`, or `tcp:7001 closed`, Maestro
73+
failed to start its on-device Android driver. The native Android runner retries
74+
that failure once with a local fallback that installs and starts the driver
75+
manually, then runs Maestro with `--no-reinstall-driver`.
76+
77+
The fallback auto-detects the Android device when exactly one device is
78+
connected. If multiple devices are connected, set `MAESTRO_ANDROID_UDID`.
79+
To force the fallback path manually, run:
80+
81+
```
82+
MAESTRO_ANDROID_UDID=emulator-5556 MAESTRO_ANDROID_MANUAL_DRIVER=1 ./scripts/e2e_maestro_android
83+
```
84+
85+
The fallback auto-detects Homebrew formula installs of Maestro. For other
86+
install layouts, set `MAESTRO_CLIENT_JAR` to the local `maestro-client.jar`.
87+
Set `MAESTRO_ANDROID_AUTO_DRIVER_FALLBACK=0` to disable the automatic retry.
88+
6489
## Adding a flow
6590

6691
1. Drop a new `<name>.yaml` under the right folder.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
appId: com.shopify.checkout_kit_mobile_buy_integration_sample
2+
name: Checkout completes
3+
tags:
4+
- android
5+
- checkout
6+
7+
# Override these for store-specific product and shipping-address data.
8+
env:
9+
PRODUCT_INDEX: ${PRODUCT_INDEX || "1"}
10+
COUNTRY: ${COUNTRY || "United States"}
11+
ADDRESS_LINE1: ${ADDRESS_LINE1 || "350 5th Ave"}
12+
CITY: ${CITY || "New York"}
13+
POSTAL_CODE: ${POSTAL_CODE || "10118"}
14+
POSTAL_FIELD: ${POSTAL_FIELD || "ZIP code"}
15+
---
16+
# Timeout tiers:
17+
# 10000 - in-app interactions (taps, animations)
18+
# 30000 - checkout step transitions (network)
19+
# 60000 - cold starts, first checkout paint, final confirmation
20+
21+
# Product and cart
22+
- launchApp:
23+
clearState: true
24+
- extendedWaitUntil:
25+
visible:
26+
id: products-tab
27+
timeout: 60000
28+
- tapOn:
29+
id: products-tab
30+
- extendedWaitUntil:
31+
visible:
32+
id: product-0-grid-item
33+
timeout: 60000
34+
- scrollUntilVisible:
35+
element:
36+
id: product-${PRODUCT_INDEX}-grid-item
37+
direction: DOWN
38+
timeout: 10000
39+
centerElement: true
40+
- tapOn:
41+
id: product-${PRODUCT_INDEX}-grid-item
42+
- scrollUntilVisible:
43+
element:
44+
id: add-to-cart-button
45+
direction: DOWN
46+
timeout: 30000
47+
centerElement: true
48+
- tapOn:
49+
id: add-to-cart-button
50+
enabled: true
51+
- waitForAnimationToEnd:
52+
timeout: 10000
53+
- tapOn:
54+
id: cart-tab
55+
- extendedWaitUntil:
56+
visible:
57+
id: checkout-button
58+
timeout: 30000
59+
- tapOn:
60+
id: checkout-button
61+
enabled: true
62+
63+
# Contact
64+
- extendedWaitUntil:
65+
visible:
66+
text: "^Email( or mobile phone number)?$"
67+
timeout: 60000
68+
- tapOn:
69+
text: "^Email( or mobile phone number)?$"
70+
- inputText: "maestro.e2e@shopify.com"
71+
- hideKeyboard
72+
- tapOn:
73+
text: "^First name( \\(optional\\))?$"
74+
- inputText: "Maestro"
75+
- hideKeyboard
76+
- tapOn:
77+
text: "^Last name$"
78+
- inputText: "Shopify"
79+
- hideKeyboard
80+
81+
# Shipping address
82+
- scrollUntilVisible:
83+
element:
84+
text: "Country/Region"
85+
direction: DOWN
86+
timeout: 10000
87+
- tapOn:
88+
text: "Country/Region"
89+
index: 1
90+
- waitForAnimationToEnd:
91+
timeout: 3000
92+
- scrollUntilVisible:
93+
element:
94+
text: "^${COUNTRY}$"
95+
direction: UP
96+
timeout: 10000
97+
visibilityPercentage: 50
98+
centerElement: true
99+
optional: true
100+
- runFlow:
101+
when:
102+
notVisible: "^${COUNTRY}$"
103+
commands:
104+
- scrollUntilVisible:
105+
element:
106+
text: "^${COUNTRY}$"
107+
direction: DOWN
108+
timeout: 30000
109+
visibilityPercentage: 50
110+
centerElement: true
111+
- tapOn:
112+
text: "^${COUNTRY}$"
113+
- waitForAnimationToEnd:
114+
timeout: 3000
115+
116+
- scrollUntilVisible:
117+
element:
118+
text: "Address"
119+
direction: DOWN
120+
timeout: 10000
121+
- tapOn:
122+
text: "Address"
123+
index: -1
124+
- eraseText: 80
125+
- scrollUntilVisible:
126+
element:
127+
text: "^${POSTAL_FIELD}$"
128+
direction: DOWN
129+
timeout: 10000
130+
centerElement: true
131+
- inputText: "${ADDRESS_LINE1} ${CITY} ${POSTAL_CODE}"
132+
- extendedWaitUntil:
133+
visible: ".*${ADDRESS_LINE1}, ${CITY}.*${POSTAL_CODE}.*${COUNTRY}.*"
134+
timeout: 30000
135+
- waitForAnimationToEnd:
136+
timeout: 2000
137+
- tapOn:
138+
text: ".*${ADDRESS_LINE1}, ${CITY}.*${POSTAL_CODE}.*${COUNTRY}.*"
139+
index: 0
140+
retryTapIfNoChange: true
141+
- waitForAnimationToEnd:
142+
timeout: 5000
143+
- extendedWaitUntil:
144+
visible: "^${POSTAL_CODE}$"
145+
timeout: 15000
146+
- hideKeyboard
147+
- waitForAnimationToEnd:
148+
timeout: 5000
149+
150+
# Payment
151+
- scrollUntilVisible:
152+
element:
153+
text: "^Card number$"
154+
direction: DOWN
155+
timeout: 30000
156+
centerElement: true
157+
- tapOn:
158+
text: "^Card number$"
159+
- inputText: "4242424242424242"
160+
- hideKeyboard
161+
- tapOn: "Expiration date (MM / YY)"
162+
- inputText: "1230"
163+
- hideKeyboard
164+
- tapOn:
165+
text: "^Security code$"
166+
- inputText: "123"
167+
- hideKeyboard
168+
- scrollUntilVisible:
169+
element:
170+
text: "^Name on card$"
171+
direction: DOWN
172+
timeout: 30000
173+
centerElement: true
174+
- scrollUntilVisible:
175+
element:
176+
text: "^Pay now$"
177+
direction: DOWN
178+
timeout: 30000
179+
- extendedWaitUntil:
180+
visible: "^Pay now$"
181+
timeout: 30000
182+
- tapOn:
183+
text: "^Pay now$"
184+
enabled: true
185+
- extendedWaitUntil:
186+
visible: ".*(Thank you|[Oo]rder (is )?confirmed).*"
187+
timeout: 60000

platforms/android/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_kit_mobile_buy_integration_sample/CheckoutKitApp.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ import androidx.compose.runtime.setValue
5454
import androidx.compose.ui.Modifier
5555
import androidx.compose.ui.layout.ContentScale
5656
import androidx.compose.ui.platform.LocalContext
57+
import androidx.compose.ui.platform.testTag
5758
import androidx.compose.ui.res.painterResource
5859
import androidx.compose.ui.res.stringResource
60+
import androidx.compose.ui.semantics.semantics
61+
import androidx.compose.ui.semantics.testTagsAsResourceId
5962
import androidx.compose.ui.unit.dp
6063
import androidx.navigation.compose.rememberNavController
6164
import com.shopify.checkout_kit_mobile_buy_integration_sample.cart.CartViewModel
@@ -98,7 +101,11 @@ fun CheckoutKitAppRoot(
98101

99102
CheckoutKitSampleTheme(darkTheme = useDarkTheme) {
100103
Surface(
101-
modifier = Modifier.fillMaxSize(),
104+
modifier = Modifier
105+
.fillMaxSize()
106+
.semantics {
107+
testTagsAsResourceId = true
108+
},
102109
) {
103110
val navController = rememberNavController()
104111
var currentScreen by remember { mutableStateOf<Screen>(Screen.Product) }
@@ -139,9 +146,12 @@ fun CheckoutKitAppRoot(
139146
)
140147
},
141148
actions = {
142-
IconButton(onClick = {
143-
navController.navigate(Screen.Cart.route)
144-
}) {
149+
IconButton(
150+
modifier = Modifier.testTag("cart-tab"),
151+
onClick = {
152+
navController.navigate(Screen.Cart.route)
153+
}
154+
) {
145155
BadgedBox(badge = {
146156
if (totalQuantity > 0) {
147157
Badge(

platforms/android/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_kit_mobile_buy_integration_sample/cart/CartView.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue
5252
import androidx.compose.ui.Alignment
5353
import androidx.compose.ui.Modifier
5454
import androidx.compose.ui.graphics.RectangleShape
55+
import androidx.compose.ui.platform.testTag
5556
import androidx.compose.ui.res.stringResource
5657
import androidx.compose.ui.text.style.TextAlign
5758
import androidx.compose.ui.text.style.TextDecoration
@@ -248,6 +249,7 @@ private fun CheckoutButton(
248249
modifier = modifier
249250
) {
250251
Button(
252+
modifier = Modifier.testTag("checkout-button"),
251253
shape = RectangleShape,
252254
onClick = onClick,
253255
) {

platforms/android/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_kit_mobile_buy_integration_sample/common/navigation/BottomAppBarWithNavigation.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable
3535
import androidx.compose.ui.Alignment
3636
import androidx.compose.ui.Modifier
3737
import androidx.compose.ui.graphics.vector.ImageVector
38+
import androidx.compose.ui.platform.testTag
3839
import androidx.compose.ui.res.stringResource
3940
import androidx.compose.ui.res.vectorResource
4041
import androidx.compose.ui.semantics.contentDescription
@@ -62,20 +63,23 @@ fun BottomAppBarWithNavigation(
6263
ImageVector.vectorResource(R.drawable.home),
6364
stringResource(id = R.string.navigation_home),
6465
currentScreen,
66+
"home-tab",
6567
)
6668
NavigationItem(
6769
navController,
6870
Screen.Products,
6971
ImageVector.vectorResource(R.drawable.product),
7072
stringResource(id = R.string.navigation_shop),
7173
currentScreen,
74+
"products-tab",
7275
)
7376
NavigationItem(
7477
navController,
7578
Screen.Settings,
7679
ImageVector.vectorResource(R.drawable.profile),
7780
stringResource(id = R.string.navigation_log_in),
7881
currentScreen,
82+
"settings-tab",
7983
)
8084
}
8185
}
@@ -88,6 +92,7 @@ fun NavigationItem(
8892
icon: ImageVector,
8993
label: String,
9094
currentScreen: Screen,
95+
testTag: String,
9196
) {
9297
val isActiveScreen = currentScreen == screen
9398
val color = if (isActiveScreen) {
@@ -99,9 +104,11 @@ fun NavigationItem(
99104
Column {
100105
IconButton(
101106
onClick = { navController.navigate(screen.route) },
102-
modifier = Modifier.semantics {
103-
this.contentDescription = "$label icon"
104-
}
107+
modifier = Modifier
108+
.testTag(testTag)
109+
.semantics {
110+
this.contentDescription = "$label icon"
111+
}
105112
) {
106113
Icon(imageVector = icon, contentDescription = label, tint = color)
107114
}

platforms/android/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_kit_mobile_buy_integration_sample/products/ProductsView.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
4040
import androidx.compose.runtime.remember
4141
import androidx.compose.ui.Alignment
4242
import androidx.compose.ui.Modifier
43+
import androidx.compose.ui.platform.testTag
4344
import androidx.compose.ui.res.stringResource
4445
import androidx.compose.ui.unit.Dp
4546
import androidx.compose.ui.unit.dp
@@ -119,6 +120,7 @@ fun ProductsView(
119120
Product(
120121
product = product,
121122
imageHeight = if (largeScreen) defaultProductImageHeightLg else defaultProductImageHeight,
123+
testTag = "product-${index}-grid-item",
122124
onProductClick = { productId ->
123125
productsViewModel.productClicked(navController, productId)
124126
}
@@ -135,10 +137,12 @@ fun ProductsView(
135137
fun Product(
136138
product: Product,
137139
imageHeight: Dp,
140+
testTag: String,
138141
onProductClick: (id: ID) -> Unit,
139142
) {
140143
Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier
141144
.wrapContentWidth()
145+
.testTag(testTag)
142146
.clickable {
143147
onProductClick(product.id)
144148
}) {

platforms/android/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_kit_mobile_buy_integration_sample/products/product/AddToCartButton.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable
3232
import androidx.compose.ui.Alignment
3333
import androidx.compose.ui.Modifier
3434
import androidx.compose.ui.graphics.RectangleShape
35+
import androidx.compose.ui.platform.testTag
3536
import androidx.compose.ui.res.stringResource
3637
import androidx.compose.ui.text.style.TextAlign
3738
import androidx.compose.ui.unit.dp
@@ -50,7 +51,9 @@ fun AddToCartButton(
5051
horizontalAlignment = Alignment.CenterHorizontally,
5152
) {
5253
TextButton(
53-
modifier = Modifier.fillMaxWidth(),
54+
modifier = Modifier
55+
.fillMaxWidth()
56+
.testTag("add-to-cart-button"),
5457
enabled = !loading,
5558
onClick = onClick,
5659
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onBackground),

0 commit comments

Comments
 (0)