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
79 changes: 71 additions & 8 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,75 @@
# Checkout Kit End-to-End Tests
# Checkout Kit, end-to-end tests

This directory is reserved for cross-platform end-to-end tests. There is no runnable e2e suite checked in yet.
Cross-platform e2e flows driven by [Maestro](https://maestro.mobile.dev).

Planned coverage:
## Layout

- Swift checkout presentation and protocol lifecycle.
- Android checkout presentation and protocol lifecycle.
- React Native wrapper behavior.
- Web component open/close and `checkout:*` events.
Tests are grouped by the sample app they exercise. Each sample app lives under
[`platforms/<name>/`](../platforms/) and has a matching folder here.

Until this directory contains test code, use the platform test suites and sample apps described in each platform README.
```
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: animation settles ~3s, local in-page
interactions and optional probes ~5s, sample-app checkout transitions ~15s,
and cold starts, checkout first-paint, and final submit ~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.
3 changes: 3 additions & 0 deletions e2e/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
platform:
ios:
snapshotKeyHonorModalViews: true
237 changes: 237 additions & 0 deletions e2e/react-native/ios/checkout-completion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
appId: com.shopify.example.CheckoutKitReactNative
name: Checkout submits and shows result
tags:
- ios
- checkout

env:
# CI shipping fixture
COUNTRY_LABEL: "United States"
ADDRESS_LINE1: "700 S Flower St"
CITY: "Los Angeles"
STATE_FIELD_LABEL: "State"
STATE_LABEL: "California"
POSTAL_CODE: "90017"
POSTAL_FIELD_LABEL: "ZIP code"

# CI payment fixture
CARD_NUMBER: "1"
CARD_EXPIRY: "1230"
CARD_SECURITY_CODE: "123"

# Accepted terminal checkout states for this smoke test.
POST_SUBMIT_RESULT_PATTERN: ".*(Thank you|Your order|Order confirmed|confirmation|Shipping not available|There was a problem|declined|couldn.t process).*"
---
# Timeout tiers:
# 3000 - animation settles
# 5000 - local in-page interactions and optional probes
# 15000 - sample-app checkout transitions
# 60000 - cold starts, first checkout paint, final submit

# 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-0-add-to-cart-button
direction: DOWN
timeout: 5000
centerElement: true
- tapOn:
id: product-0-add-to-cart-button
enabled: true
- waitForAnimationToEnd:
timeout: 3000
- runFlow:
when:
visible:
id: header-cart-icon
commands:
- tapOn:
id: header-cart-icon
- runFlow:
when:
notVisible:
id: checkout-button
commands:
- tapOn:
id: cart-tab
- extendedWaitUntil:
visible:
id: checkout-button
timeout: 15000
- 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: 5000
- tapOn:
text: "Country/Region"
index: 1
- waitForAnimationToEnd:
timeout: 3000
- scrollUntilVisible:
element:
text: "^${COUNTRY_LABEL}$"
direction: UP
timeout: 5000
visibilityPercentage: 10
optional: true
- scrollUntilVisible:
element:
text: "^${COUNTRY_LABEL}$"
direction: DOWN
timeout: 5000
visibilityPercentage: 10
optional: true
- tapOn:
text: "^${COUNTRY_LABEL}$"
- waitForAnimationToEnd:
timeout: 3000

- scrollUntilVisible:
element:
text: "Address"
direction: DOWN
timeout: 5000
- tapOn:
text: "Address"
index: -1
- eraseText: 80
- inputText: "${ADDRESS_LINE1}"
- tapOn: "selected"
- scrollUntilVisible:
element:
text: "^City$"
direction: DOWN
timeout: 5000
centerElement: true
- tapOn:
text: "^City$"
index: -1
- eraseText: 80
- inputText: "${CITY}"
- tapOn: "selected"
- scrollUntilVisible:
element:
text: "^${STATE_FIELD_LABEL}$"
direction: DOWN
timeout: 5000
centerElement: true
- tapOn:
text: "^${STATE_FIELD_LABEL}$"
index: -1
- waitForAnimationToEnd:
timeout: 3000
- scrollUntilVisible:
element:
text: "^${STATE_LABEL}$"
direction: UP
timeout: 5000
visibilityPercentage: 100
optional: true
- scrollUntilVisible:
element:
text: "^${STATE_LABEL}$"
direction: DOWN
timeout: 5000
visibilityPercentage: 100
optional: true
- tapOn:
text: "^${STATE_LABEL}$"
- waitForAnimationToEnd:
timeout: 3000
- extendedWaitUntil:
notVisible: "Select a state"
timeout: 5000
- extendedWaitUntil:
visible: "^${STATE_LABEL}$"
timeout: 5000
- scrollUntilVisible:
element:
text: "^${POSTAL_FIELD_LABEL}$"
direction: DOWN
timeout: 5000
centerElement: true
- tapOn:
text: "^${POSTAL_FIELD_LABEL}$"
index: -1
- eraseText: 80
- inputText: "${POSTAL_CODE}"
- tapOn: "selected"
- extendedWaitUntil:
visible: "^${POSTAL_CODE}$"
timeout: 5000
- waitForAnimationToEnd:
timeout: 3000

# Payment
- scrollUntilVisible:
element:
text: "^Field container for: Card number$"
direction: DOWN
timeout: 5000
centerElement: true
optional: true
- runFlow:
when:
visible: "^Field container for: Card number$"
commands:
- tapOn:
text: "^Field container for: Card number$"
- inputText: "${CARD_NUMBER}"
- tapOn: "selected"
- tapOn: "Expiration date (MM / YY)"
- inputText: "${CARD_EXPIRY}"
- tapOn: "selected"
- tapOn: "Field container for: Security code"
- inputText: "${CARD_SECURITY_CODE}"
- tapOn: "selected"
- scrollUntilVisible:
element:
text: "^Field container for: Name on card$"
direction: DOWN
timeout: 5000
centerElement: true
- scrollUntilVisible:
element:
text: "^(Pay now|Complete order)$"
direction: DOWN
timeout: 5000
centerElement: true
- tapOn:
text: "^(Pay now|Complete order)$"
enabled: true
- extendedWaitUntil:
visible: "${POST_SUBMIT_RESULT_PATTERN}"
timeout: 60000
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 @@ -134,6 +134,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