Skip to content
Draft
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
75 changes: 73 additions & 2 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,74 @@
# Checkout Kit — End-to-end tests
# Checkout Kit, end-to-end tests

Placeholder. End-to-end tests covering Swift, Android, and (later) React Native will be folded into this directory in a follow-up.
Cross-platform e2e flows driven by [Maestro](https://maestro.mobile.dev).

## Layout

Tests are grouped by the sample app they exercise. Each sample app lives under
[`platforms/<name>/`](../platforms/) and has a matching folder here.

```
e2e/
├── config.yaml Shared Maestro config (all platforms)
├── swift/ Targets the Swift sample (iOS only)
├── android/ Targets the Android sample (Android only)
└── react-native/ Targets the RN sample (cross-platform)
├── ios/
└── android/
```

The Swift sample is iOS-only and the Android sample is Android-only by
construction, so they don't need an inner platform split. The React Native
sample ships to both platforms; its flows are split because some assertions
are platform-specific (iOS accessibility-label patterns vs Android resource
strings).

Folders are created when their first flow lands. Don't pre-create empty
directories.

## Sample-app appIds

Use these in the `appId:` header of every flow. Don't invent new bundle ids.

| Folder | appId |
| ------------------------- | ------------------------------------------------ |
| `swift/` | `com.shopify.example.MobileBuyIntegration` |
| `android/` | `com.shopify.checkout_kit_mobile_buy_integration_sample` |
| `react-native/ios/` | `com.shopify.example.CheckoutKitReactNative` |
| `react-native/android/` | `com.shopify.example.CheckoutKitReactNative` |

## Running

Each platform's runner script lives next to its sample app. Build and launch
the sample on a simulator/emulator first, then run the script in a second
terminal.

| Platform | From | Command |
| ------------------ | ------------------------------- | ------------------ |
| React Native, iOS | `platforms/react-native/` | `pnpm e2e:ios` |
| Swift, iOS | TBD | TBD |
| Android (native) | TBD | TBD |
| RN, Android | TBD | TBD |

Maestro itself is a system CLI, not an npm dependency. Install once with:

```
curl -fsSL "https://get.maestro.mobile.dev" | bash
```

## Adding a flow

1. Drop a new `<name>.yaml` under the right folder.
2. Set `appId:` from the table above.
3. Keep timeouts in the existing tiers (in-app interactions ~10s, network
transitions ~30s, cold starts and checkout first-paint ~60s).
4. If the flow needs an npm script wrapper, add an `e2e:<platform>` script to
the matching `package.json` next to existing scripts. The script should
point at the folder, not an individual file, so the whole folder runs.

## Required sample-app accessibility

Maestro flows rely on testIDs / accessibility labels in the sample apps. When
adding a flow, prefer querying by `id:` (stable, controlled by us) over
`text:` (fragile, depends on storefront copy). If a tappable element doesn't
have an id, add one to the sample first, in a separate commit.
2 changes: 2 additions & 0 deletions e2e/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ios:
snapshotKeyHonorModalViews: true
171 changes: 171 additions & 0 deletions e2e/react-native/ios/checkout-completion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
appId: com.shopify.example.CheckoutKitReactNative
name: Checkout reaches Pay now
tags:
- ios
- checkout

# Override these for store-specific product and shipping-address data.
env:
PRODUCT_INDEX: ${PRODUCT_INDEX || "1"}
COUNTRY: ${COUNTRY || "United States"}
ADDRESS_LINE1: ${ADDRESS_LINE1 || "350 5th Ave"}
CITY: ${CITY || "New York"}
POSTAL_CODE: ${POSTAL_CODE || "10118"}
POSTAL_FIELD: ${POSTAL_FIELD || "ZIP code"}
---
# Timeout tiers:
# 10000 - in-app interactions (taps, animations)
# 30000 - checkout step transitions (network)
# 60000 - cold starts, first checkout paint

# Product and cart
- launchApp:
clearState: true
arguments:
AppleLocale: en_US
AppleLanguages: "(en)"
- extendedWaitUntil:
visible:
id: product-0-add-to-cart-button
timeout: 60000
- scrollUntilVisible:
element:
id: product-${PRODUCT_INDEX}-add-to-cart-button
direction: DOWN
timeout: 10000
centerElement: true
- tapOn:
id: product-${PRODUCT_INDEX}-add-to-cart-button
enabled: true
- waitForAnimationToEnd:
timeout: 10000
- tapOn:
id: cart-tab
- extendedWaitUntil:
visible:
id: checkout-button
timeout: 30000
- tapOn:
id: checkout-button
enabled: true

# Contact
- extendedWaitUntil:
visible:
text: "^Email( or mobile phone number)?$"
timeout: 60000
- tapOn:
text: "^Email( or mobile phone number)?$"
- inputText: "maestro.e2e@shopify.com"
- tapOn: "selected"
- tapOn:
text: "^First name( \\(optional\\))?$"
- inputText: "Maestro"
- tapOn: "selected"
- tapOn:
text: "^Last name$"
- inputText: "Shopify"
- tapOn: "selected"

# Shipping address
- scrollUntilVisible:
element:
text: "Country/Region"
direction: DOWN
timeout: 10000
- tapOn:
text: "Country/Region"
index: 1
- waitForAnimationToEnd:
timeout: 3000
- scrollUntilVisible:
element:
text: "^${COUNTRY}$"
direction: UP
timeout: 10000
visibilityPercentage: 50
centerElement: true
optional: true
- runFlow:
when:
notVisible: "^${COUNTRY}$"
commands:
- scrollUntilVisible:
element:
text: "^${COUNTRY}$"
direction: DOWN
timeout: 30000
visibilityPercentage: 50
centerElement: true
- tapOn:
text: "^${COUNTRY}$"
- waitForAnimationToEnd:
timeout: 3000

- scrollUntilVisible:
element:
text: "Address"
direction: DOWN
timeout: 10000
- tapOn:
text: "Address"
index: -1
- eraseText: 80
- scrollUntilVisible:
element:
text: "^${POSTAL_FIELD}$"
direction: DOWN
timeout: 10000
centerElement: true
- inputText: "${ADDRESS_LINE1} ${CITY} ${POSTAL_CODE}"
- extendedWaitUntil:
visible: ".*${ADDRESS_LINE1}, ${CITY}.*${POSTAL_CODE}.*${COUNTRY}.*"
timeout: 30000
- waitForAnimationToEnd:
timeout: 2000
- tapOn:
text: ".*${ADDRESS_LINE1}, ${CITY}.*${POSTAL_CODE}.*${COUNTRY}.*"
index: 0
retryTapIfNoChange: true
- waitForAnimationToEnd:
timeout: 5000
- extendedWaitUntil:
visible: "^${POSTAL_CODE}$"
timeout: 15000
- tapOn: "selected"
- waitForAnimationToEnd:
timeout: 5000
# iOS intentionally stops at Pay now because this store/address path can show
# a checkout-web "Shipping not available" banner after submit.

# Payment
- scrollUntilVisible:
element:
text: "^Field container for: Card number$"
direction: DOWN
timeout: 30000
centerElement: true
- tapOn:
text: "^Field container for: Card number$"
- inputText: "4242424242424242"
- tapOn: "selected"
- tapOn: "Expiration date (MM / YY)"
- inputText: "1230"
- tapOn: "selected"
- tapOn: "Field container for: Security code"
- inputText: "123"
- tapOn: "selected"
- scrollUntilVisible:
element:
text: "^Field container for: Name on card$"
direction: DOWN
timeout: 30000
centerElement: true
- scrollUntilVisible:
element:
text: "^Pay now$"
direction: DOWN
timeout: 30000
- extendedWaitUntil:
visible: "^Pay now$"
timeout: 30000
3 changes: 2 additions & 1 deletion platforms/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"snapshot": "./scripts/create_snapshot",
"compare-snapshot": "./scripts/compare_snapshot",
"turbo": "turbo",
"test": "jest"
"test": "jest",
"e2e:ios": "maestro --platform ios test --config ../../e2e/config.yaml ../../e2e/react-native/ios"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ function Product({
</View>
) : (
<Pressable
testID={`${testID}-add-to-cart-button`}
style={styles.addToCartButton}
onPress={() => variant?.id && onAddToCart(variant.id)}>
<Text style={styles.addToCartButtonText}>Add to cart</Text>
Expand Down
Loading