Skip to content

Commit 7d77bc3

Browse files
committed
Add React Native iOS checkout e2e flow
1 parent 1879b3a commit 7d77bc3

7 files changed

Lines changed: 362 additions & 14 deletions

File tree

e2e/README.md

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,75 @@
1-
# Checkout Kit End-to-End Tests
1+
# Checkout Kit, end-to-end tests
22

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

5-
Planned coverage:
5+
## Layout
66

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

12-
Until this directory contains test code, use the platform test suites and sample apps described in each platform README.
10+
```
11+
e2e/
12+
├── config.yaml Shared Maestro config (all platforms)
13+
├── swift/ Targets the Swift sample (iOS only)
14+
├── android/ Targets the Android sample (Android only)
15+
└── react-native/ Targets the RN sample (cross-platform)
16+
├── ios/
17+
└── android/
18+
```
19+
20+
The Swift sample is iOS-only and the Android sample is Android-only by
21+
construction, so they don't need an inner platform split. The React Native
22+
sample ships to both platforms; its flows are split because some assertions
23+
are platform-specific (iOS accessibility-label patterns vs Android resource
24+
strings).
25+
26+
Folders are created when their first flow lands. Don't pre-create empty
27+
directories.
28+
29+
## Sample-app appIds
30+
31+
Use these in the `appId:` header of every flow. Don't invent new bundle ids.
32+
33+
| Folder | appId |
34+
| ------------------------- | ------------------------------------------------ |
35+
| `swift/` | `com.shopify.example.MobileBuyIntegration` |
36+
| `android/` | `com.shopify.checkout_kit_mobile_buy_integration_sample` |
37+
| `react-native/ios/` | `com.shopify.example.CheckoutKitReactNative` |
38+
| `react-native/android/` | `com.shopify.example.CheckoutKitReactNative` |
39+
40+
## Running
41+
42+
Each platform's runner script lives next to its sample app. Build and launch
43+
the sample on a simulator/emulator first, then run the script in a second
44+
terminal.
45+
46+
| Platform | From | Command |
47+
| ------------------ | ------------------------------- | ------------------ |
48+
| React Native, iOS | `platforms/react-native/` | `pnpm e2e:ios` |
49+
| Swift, iOS | TBD | TBD |
50+
| Android (native) | TBD | TBD |
51+
| RN, Android | TBD | TBD |
52+
53+
Maestro itself is a system CLI, not an npm dependency. Install once with:
54+
55+
```
56+
curl -fsSL "https://get.maestro.mobile.dev" | bash
57+
```
58+
59+
## Adding a flow
60+
61+
1. Drop a new `<name>.yaml` under the right folder.
62+
2. Set `appId:` from the table above.
63+
3. Keep timeouts in the existing tiers: animation settles ~3s, local in-page
64+
interactions and optional probes ~5s, sample-app checkout transitions ~15s,
65+
and cold starts, checkout first-paint, and final submit ~60s.
66+
4. If the flow needs an npm script wrapper, add an `e2e:<platform>` script to
67+
the matching `package.json` next to existing scripts. The script should
68+
point at the folder, not an individual file, so the whole folder runs.
69+
70+
## Required sample-app accessibility
71+
72+
Maestro flows rely on testIDs / accessibility labels in the sample apps. When
73+
adding a flow, prefer querying by `id:` (stable, controlled by us) over
74+
`text:` (fragile, depends on storefront copy). If a tappable element doesn't
75+
have an id, add one to the sample first, in a separate commit.

e2e/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
platform:
2+
ios:
3+
# Lets Maestro inspect elements presented inside iOS checkout modal views.
4+
snapshotKeyHonorModalViews: true
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
appId: com.shopify.example.CheckoutKitReactNative
2+
name: Checkout submits and shows result
3+
tags:
4+
- ios
5+
- checkout
6+
7+
env:
8+
PRODUCT_INDEX: "0"
9+
10+
# Checkout contact fixture
11+
EMAIL: "maestro.e2e@shopify.com"
12+
FIRST_NAME: "Maestro"
13+
LAST_NAME: "Shopify"
14+
15+
# Checkout shipping fixture
16+
COUNTRY_LABEL: "United States"
17+
ADDRESS_LINE1: "700 S Flower St"
18+
CITY: "Los Angeles"
19+
STATE_FIELD_LABEL: "State"
20+
STATE_LABEL: "California"
21+
POSTAL_CODE: "90017"
22+
POSTAL_FIELD_LABEL: "ZIP code"
23+
24+
# Checkout payment fixture
25+
CARD_NUMBER: "1"
26+
CARD_EXPIRY: "1230"
27+
CARD_SECURITY_CODE: "123"
28+
29+
# Accepted successful checkout states for this smoke test.
30+
POST_SUBMIT_RESULT_PATTERN: ".*(Thank you|Your order|Order confirmed|confirmation).*"
31+
---
32+
# Timeout tiers:
33+
# 3000 - animation settles
34+
# 5000 - local in-page interactions and optional probes
35+
# 15000 - sample-app checkout transitions
36+
# 60000 - cold starts, first checkout paint, final submit
37+
38+
# Product and cart
39+
- launchApp:
40+
clearState: true
41+
arguments:
42+
AppleLocale: en_US
43+
AppleLanguages: "(en)"
44+
- extendedWaitUntil:
45+
visible:
46+
id: product-${PRODUCT_INDEX}-add-to-cart-button
47+
timeout: 60000
48+
- scrollUntilVisible:
49+
element:
50+
id: product-${PRODUCT_INDEX}-add-to-cart-button
51+
direction: DOWN
52+
timeout: 5000
53+
centerElement: true
54+
- tapOn:
55+
id: product-${PRODUCT_INDEX}-add-to-cart-button
56+
enabled: true
57+
- waitForAnimationToEnd:
58+
timeout: 3000
59+
- runFlow:
60+
when:
61+
visible:
62+
id: header-cart-icon
63+
commands:
64+
- tapOn:
65+
id: header-cart-icon
66+
- runFlow:
67+
when:
68+
notVisible:
69+
id: checkout-button
70+
commands:
71+
- tapOn:
72+
id: cart-tab
73+
- extendedWaitUntil:
74+
visible:
75+
id: checkout-button
76+
timeout: 15000
77+
- tapOn:
78+
id: checkout-button
79+
enabled: true
80+
81+
# Contact
82+
- extendedWaitUntil:
83+
visible:
84+
text: "^Email( or mobile phone number)?$"
85+
timeout: 60000
86+
- tapOn:
87+
text: "^Email( or mobile phone number)?$"
88+
- inputText: "${EMAIL}"
89+
- tapOn: "selected"
90+
- tapOn:
91+
text: "^First name( \\(optional\\))?$"
92+
- inputText: "${FIRST_NAME}"
93+
- tapOn: "selected"
94+
- tapOn:
95+
text: "^Last name$"
96+
- inputText: "${LAST_NAME}"
97+
- tapOn: "selected"
98+
99+
# Shipping address
100+
- scrollUntilVisible:
101+
element:
102+
text: "Country/Region"
103+
direction: DOWN
104+
timeout: 5000
105+
- tapOn:
106+
text: "Country/Region"
107+
index: 1
108+
- waitForAnimationToEnd:
109+
timeout: 3000
110+
- scrollUntilVisible:
111+
element:
112+
text: "^${COUNTRY_LABEL}$"
113+
direction: UP
114+
timeout: 5000
115+
visibilityPercentage: 10
116+
optional: true
117+
- scrollUntilVisible:
118+
element:
119+
text: "^${COUNTRY_LABEL}$"
120+
direction: DOWN
121+
timeout: 5000
122+
visibilityPercentage: 10
123+
optional: true
124+
- tapOn:
125+
text: "^${COUNTRY_LABEL}$"
126+
- waitForAnimationToEnd:
127+
timeout: 3000
128+
129+
- scrollUntilVisible:
130+
element:
131+
text: "Address"
132+
direction: DOWN
133+
timeout: 5000
134+
- tapOn:
135+
text: "Address"
136+
index: -1
137+
- eraseText: 80
138+
- inputText: "${ADDRESS_LINE1}"
139+
- tapOn: "selected"
140+
- scrollUntilVisible:
141+
element:
142+
text: "^City$"
143+
direction: DOWN
144+
timeout: 5000
145+
centerElement: true
146+
- tapOn:
147+
text: "^City$"
148+
index: -1
149+
- eraseText: 80
150+
- inputText: "${CITY}"
151+
- tapOn: "selected"
152+
- scrollUntilVisible:
153+
element:
154+
text: "^${STATE_FIELD_LABEL}$"
155+
direction: DOWN
156+
timeout: 5000
157+
centerElement: true
158+
- tapOn:
159+
text: "^${STATE_FIELD_LABEL}$"
160+
index: -1
161+
- waitForAnimationToEnd:
162+
timeout: 3000
163+
- scrollUntilVisible:
164+
element:
165+
text: "^${STATE_LABEL}$"
166+
direction: UP
167+
timeout: 5000
168+
visibilityPercentage: 100
169+
optional: true
170+
- scrollUntilVisible:
171+
element:
172+
text: "^${STATE_LABEL}$"
173+
direction: DOWN
174+
timeout: 5000
175+
visibilityPercentage: 100
176+
optional: true
177+
- tapOn:
178+
text: "^${STATE_LABEL}$"
179+
- waitForAnimationToEnd:
180+
timeout: 3000
181+
- extendedWaitUntil:
182+
notVisible: "Select a state"
183+
timeout: 5000
184+
- extendedWaitUntil:
185+
visible: "^${STATE_LABEL}$"
186+
timeout: 5000
187+
- scrollUntilVisible:
188+
element:
189+
text: "^${POSTAL_FIELD_LABEL}$"
190+
direction: DOWN
191+
timeout: 5000
192+
centerElement: true
193+
- tapOn:
194+
text: "^${POSTAL_FIELD_LABEL}$"
195+
index: -1
196+
- eraseText: 80
197+
- inputText: "${POSTAL_CODE}"
198+
- tapOn: "selected"
199+
- extendedWaitUntil:
200+
visible: "^${POSTAL_CODE}$"
201+
timeout: 5000
202+
- waitForAnimationToEnd:
203+
timeout: 3000
204+
205+
# Payment
206+
- scrollUntilVisible:
207+
element:
208+
text: "^Field container for: Card number$"
209+
direction: DOWN
210+
timeout: 5000
211+
centerElement: true
212+
optional: true
213+
- runFlow:
214+
when:
215+
visible: "^Field container for: Card number$"
216+
commands:
217+
- tapOn:
218+
text: "^Field container for: Card number$"
219+
- inputText: "${CARD_NUMBER}"
220+
- tapOn: "selected"
221+
- tapOn: "Expiration date (MM / YY)"
222+
- inputText: "${CARD_EXPIRY}"
223+
- tapOn: "selected"
224+
- tapOn: "Field container for: Security code"
225+
- inputText: "${CARD_SECURITY_CODE}"
226+
- tapOn: "selected"
227+
- scrollUntilVisible:
228+
element:
229+
text: "^Field container for: Name on card$"
230+
direction: DOWN
231+
timeout: 5000
232+
centerElement: true
233+
- scrollUntilVisible:
234+
element:
235+
text: "^(Pay now|Complete order)$"
236+
direction: DOWN
237+
timeout: 5000
238+
centerElement: true
239+
- tapOn:
240+
text: "^(Pay now|Complete order)$"
241+
enabled: true
242+
- extendedWaitUntil:
243+
visible: "${POST_SUBMIT_RESULT_PATTERN}"
244+
timeout: 60000
245+
- tapOn: "close"
246+
- extendedWaitUntil:
247+
visible: "^Your cart is empty\\.$"
248+
timeout: 15000

platforms/react-native/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"snapshot": "./scripts/create_snapshot",
2525
"compare-snapshot": "./scripts/compare_snapshot",
2626
"turbo": "turbo",
27-
"test": "jest"
27+
"test": "jest",
28+
"e2e:ios": "maestro --platform ios test --config ../../e2e/config.yaml ../../e2e/react-native/ios"
2829
},
2930
"devDependencies": {
3031
"@babel/core": "^7.25.2",

platforms/react-native/sample/src/hooks/useCheckoutEventHandlers.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ interface EventHandlers {
1515
onClickLink?: (url: string) => void;
1616
}
1717

18-
export function useShopifyProtocolEventHandlers(name?: string): ProtocolHandlers {
18+
export function useShopifyProtocolEventHandlers(
19+
name?: string,
20+
additionalHandlers: Partial<ProtocolHandlers> = {},
21+
): ProtocolHandlers {
1922
const log = createDebugLogger(name ?? '');
2023

2124
// Keep the sample subscribed to every public protocol event automatically.
@@ -26,6 +29,11 @@ export function useShopifyProtocolEventHandlers(name?: string): ProtocolHandlers
2629
>((handlers, method) => {
2730
handlers[method] = payload => {
2831
log(method, payload);
32+
(
33+
additionalHandlers[method as keyof ProtocolHandlers] as
34+
| ((payload: unknown) => void)
35+
| undefined
36+
)?.(payload);
2937
};
3038
return handlers;
3139
}, {}) as ProtocolHandlers;

0 commit comments

Comments
 (0)