From c674693d4eeed43bcf56151bcc577fa717e2573a Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Sun, 17 May 2026 10:59:00 -0400 Subject: [PATCH] Add React Native iOS checkout e2e flow --- e2e/README.md | 79 +++++- e2e/config.yaml | 3 + e2e/react-native/ios/checkout-completion.yaml | 237 ++++++++++++++++++ platforms/react-native/package.json | 3 +- .../sample/src/screens/CatalogScreen.tsx | 1 + 5 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 e2e/config.yaml create mode 100644 e2e/react-native/ios/checkout-completion.yaml diff --git a/e2e/README.md b/e2e/README.md index 0973a1c1..1dba4b6d 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -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//`](../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 `.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:` 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. diff --git a/e2e/config.yaml b/e2e/config.yaml new file mode 100644 index 00000000..75e5ed00 --- /dev/null +++ b/e2e/config.yaml @@ -0,0 +1,3 @@ +platform: + ios: + snapshotKeyHonorModalViews: true diff --git a/e2e/react-native/ios/checkout-completion.yaml b/e2e/react-native/ios/checkout-completion.yaml new file mode 100644 index 00000000..328e57e2 --- /dev/null +++ b/e2e/react-native/ios/checkout-completion.yaml @@ -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 diff --git a/platforms/react-native/package.json b/platforms/react-native/package.json index 06cd6aee..6acad1ac 100644 --- a/platforms/react-native/package.json +++ b/platforms/react-native/package.json @@ -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", diff --git a/platforms/react-native/sample/src/screens/CatalogScreen.tsx b/platforms/react-native/sample/src/screens/CatalogScreen.tsx index ed103d40..5b936cb9 100644 --- a/platforms/react-native/sample/src/screens/CatalogScreen.tsx +++ b/platforms/react-native/sample/src/screens/CatalogScreen.tsx @@ -134,6 +134,7 @@ function Product({ ) : ( variant?.id && onAddToCart(variant.id)}> Add to cart