diff --git a/e2e/README.md b/e2e/README.md index 0973a1c1..aa8ebd85 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,12 +1,45 @@ # 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. +This directory contains shared cross-platform Maestro flows for the Checkout Kit +sample apps. -Planned coverage: +The current shared smoke flow verifies the happy path from a sample app cart +into Shopify checkout and back to the app after completion. Each platform owns +the small wrapper command that supplies its app id, cart bootstrap link, and +platform-specific checkout field handling. -- Swift checkout presentation and protocol lifecycle. -- Android checkout presentation and protocol lifecycle. -- React Native wrapper behavior. -- Web component open/close and `checkout:*` events. +## Run locally -Until this directory contains test code, use the platform test suites and sample apps described in each platform README. +Build and install the target sample app first, then run the matching Maestro +command. + +From `platforms/react-native`: + +```bash +pnpm e2e:ios +pnpm e2e:android +``` + +From `platforms/swift`: + +```bash +./Scripts/e2e_maestro_ios +``` + +From `platforms/android`: + +```bash +./scripts/e2e_maestro_android +``` + +## Files + +- `config.yaml` configures Maestro for shared platform behavior. +- `shared/checkout-smoke.yaml` contains the cross-platform checkout smoke flow. + +## Scope + +This smoke flow is intended to catch regressions in the sample app integration +surface: cart bootstrap, checkout presentation, checkout completion, and return +to the sample app. It is not a replacement for checkout-web's own browser-based +test coverage. diff --git a/e2e/config.yaml b/e2e/config.yaml new file mode 100644 index 00000000..cf1076dc --- /dev/null +++ b/e2e/config.yaml @@ -0,0 +1,4 @@ +platform: + ios: + # Lets Maestro inspect elements presented inside iOS checkout modal views. + snapshotKeyHonorModalViews: true diff --git a/e2e/shared/checkout-smoke.yaml b/e2e/shared/checkout-smoke.yaml new file mode 100644 index 00000000..a1f94e2c --- /dev/null +++ b/e2e/shared/checkout-smoke.yaml @@ -0,0 +1,390 @@ +appId: ${APP_ID} +name: Shared checkout smoke + +env: + # Checkout contact fixture + EMAIL: "maestro.e2e@shopify.com" + FIRST_NAME: "Maestro" + LAST_NAME: "Shopify" + + # Checkout 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" + + # Checkout payment fixture + CARD_NUMBER: "1" + CARD_EXPIRY: "1230" + CARD_EXPIRY_DISPLAY: "12 / 30" + CARD_SECURITY_CODE: "123" + # Checkout pre-fills this from FIRST_NAME and LAST_NAME; Android asserts it. + CARDHOLDER_NAME: "Maestro Shopify" + + # Accepted successful checkout states for this smoke test. + POST_SUBMIT_RESULT_PATTERN: ".*(Thank you|Your order|Order confirmed|confirmation).*" + + # RN Android needs an extra keyboard dismissal after address entry. + DISMISS_ADDRESS_KEYBOARD_AFTER_SUGGESTIONS: false +--- +- launchApp: + clearState: true + arguments: + # iOS-only launch arguments; Android ignores them. + AppleLocale: en_US + AppleLanguages: "(en)" +- extendedWaitUntil: + visible: + id: checkout-kit-sample-ready + timeout: 60000 +- runFlow: + when: + true: ${STOP_APP_BEFORE_DEEPLINK} + commands: + # RN Android opts into this so the app receives the bootstrap link on cold start. + - stopApp +- openLink: "${CART_BOOTSTRAP_LINK}" +- runFlow: + when: + visible: "Open" + commands: + - tapOn: "Open" +- 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: "${EMAIL}" +- runFlow: + when: + visible: "selected" + commands: + - tapOn: "selected" +- runFlow: + when: + true: ${USE_ANDROID_PAYMENT_FIELDS} + commands: + - runFlow: + when: + notVisible: "^${EMAIL}$" + commands: + - tapOn: + id: email + - eraseText: 80 + - inputText: "${EMAIL}" + - extendedWaitUntil: + visible: "^${EMAIL}$" + - pressKey: Back + - waitForAnimationToEnd +- scrollUntilVisible: + element: + text: "^First name( \\(optional\\))?$" + direction: DOWN + visibilityPercentage: 100 + centerElement: true +- tapOn: + text: "^First name( \\(optional\\))?$" +- inputText: "${FIRST_NAME}" +- runFlow: + when: + visible: "selected" + commands: + - tapOn: "selected" +- scrollUntilVisible: + element: + text: "^Last name$" + direction: DOWN + visibilityPercentage: 100 + centerElement: true +- tapOn: + text: "^Last name$" +- inputText: "${LAST_NAME}" +- runFlow: + when: + visible: "selected" + commands: + - tapOn: "selected" + +# Shipping address +- scrollUntilVisible: + element: + text: "Country/Region" + direction: DOWN +- tapOn: + text: "Country/Region" + index: 1 +- waitForAnimationToEnd +- scrollUntilVisible: + element: + text: "^${COUNTRY_LABEL}$" + direction: UP + visibilityPercentage: 10 + optional: true +- scrollUntilVisible: + element: + text: "^${COUNTRY_LABEL}$" + direction: DOWN + visibilityPercentage: 10 + optional: true +- tapOn: + text: "^${COUNTRY_LABEL}$" +- waitForAnimationToEnd + +- scrollUntilVisible: + element: + text: "Address" + direction: DOWN +- tapOn: + text: "Address" + index: -1 +- eraseText: 80 +- inputText: "${ADDRESS_LINE1}" +- runFlow: + when: + visible: "selected" + commands: + - tapOn: "selected" +- runFlow: + when: + visible: "Close suggestions" + commands: + - tapOn: "Close suggestions" + - waitForAnimationToEnd +- runFlow: + when: + true: ${DISMISS_ADDRESS_KEYBOARD_AFTER_SUGGESTIONS} + commands: + - pressKey: Back + - waitForAnimationToEnd +- scrollUntilVisible: + element: + text: "^City$" + direction: DOWN + centerElement: true +- tapOn: + text: "^City$" + index: -1 +- eraseText: 80 +- inputText: "${CITY}" +- runFlow: + when: + visible: "selected" + commands: + - tapOn: "selected" +- runFlow: + when: + true: ${USE_ANDROID_PAYMENT_FIELDS} + commands: + - pressKey: Back + - waitForAnimationToEnd +- scrollUntilVisible: + element: + text: "^${STATE_FIELD_LABEL}$" + direction: DOWN + centerElement: true +- runFlow: + when: + notVisible: "^${STATE_LABEL}$" + commands: + - tapOn: + text: "^${STATE_FIELD_LABEL}$" + index: -1 + - waitForAnimationToEnd +- scrollUntilVisible: + element: + text: "^${STATE_LABEL}$" + direction: UP + visibilityPercentage: 100 + optional: true +- scrollUntilVisible: + element: + text: "^${STATE_LABEL}$" + direction: DOWN + visibilityPercentage: 100 + optional: true +- tapOn: + text: "^${STATE_LABEL}$" +- waitForAnimationToEnd +- extendedWaitUntil: + notVisible: "Select a state" +- extendedWaitUntil: + visible: "^${STATE_LABEL}$" +- scrollUntilVisible: + element: + text: "^${POSTAL_FIELD_LABEL}$" + direction: DOWN + centerElement: true +- tapOn: + text: "^${POSTAL_FIELD_LABEL}$" + index: -1 +- eraseText: 80 +- inputText: "${POSTAL_CODE}" +- runFlow: + when: + visible: "selected" + commands: + - tapOn: "selected" +- extendedWaitUntil: + visible: "^${POSTAL_CODE}$" +- waitForAnimationToEnd + +# Payment +- runFlow: + when: + true: ${USE_IOS_PAYMENT_FIELDS} + commands: + - scrollUntilVisible: + element: + text: "^Field container for: Card number$" + direction: DOWN + centerElement: true + - 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 + centerElement: true +- runFlow: + when: + true: ${USE_ANDROID_PAYMENT_FIELDS} + commands: + - scrollUntilVisible: + element: + id: number + direction: DOWN + centerElement: true + - tapOn: + text: "Card number" + index: -1 + - inputText: "${CARD_NUMBER}" + - scrollUntilVisible: + element: + id: expiry + direction: DOWN + centerElement: true + - tapOn: + id: expiry + index: -1 + # Android's hosted expiry field is entered digit-by-digit; keep in sync with CARD_EXPIRY. + - inputText: "1" + - waitForAnimationToEnd + - inputText: "2" + - waitForAnimationToEnd + - inputText: "3" + - waitForAnimationToEnd + - inputText: "0" + - extendedWaitUntil: + visible: "^${CARD_EXPIRY_DISPLAY}$" + - pressKey: Back + - waitForAnimationToEnd + - scrollUntilVisible: + element: + id: verification_value + direction: DOWN + centerElement: true + optional: true + - runFlow: + when: + visible: + id: verification_value + commands: + - tapOn: + id: verification_value + index: -1 + - runFlow: + when: + notVisible: + id: verification_value + commands: + - scrollUntilVisible: + element: + text: "Security code" + direction: DOWN + centerElement: true + - tapOn: + text: "Security code" + index: -1 + - inputText: "${CARD_SECURITY_CODE}" + - extendedWaitUntil: + visible: "^${CARD_SECURITY_CODE}$" + - pressKey: Back + - waitForAnimationToEnd + - scrollUntilVisible: + element: + id: name + direction: DOWN + centerElement: true + - extendedWaitUntil: + visible: "^${CARDHOLDER_NAME}$" +- waitForAnimationToEnd +- runFlow: + when: + true: ${USE_ANDROID_PAYMENT_FIELDS} + commands: + - scrollUntilVisible: + element: + id: checkout-pay-button + direction: DOWN + visibilityPercentage: 90 + - tapOn: + id: checkout-pay-button +- runFlow: + when: + true: ${USE_IOS_PAYMENT_FIELDS} + commands: + - tapOn: + text: "^(Pay now|Complete order)$" + enabled: true +- extendedWaitUntil: + visible: "${POST_SUBMIT_RESULT_PATTERN}" + timeout: 60000 +- runFlow: + when: + visible: "close" + commands: + - tapOn: "close" +- runFlow: + when: + visible: "Close Checkout" + commands: + - tapOn: "Close Checkout" +- runFlow: + when: + visible: + id: products-tab + commands: + - tapOn: + id: cart-tab +- runFlow: + when: + visible: + id: catalog-tab + commands: + - tapOn: + id: cart-tab +- extendedWaitUntil: + visible: + id: cart-empty-message + timeout: 15000 diff --git a/platforms/android/scripts/e2e_maestro_android b/platforms/android/scripts/e2e_maestro_android new file mode 100755 index 00000000..82a07e9e --- /dev/null +++ b/platforms/android/scripts/e2e_maestro_android @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +E2E_DIR="$(cd "$SCRIPT_DIR/../../../e2e" && pwd)" + +maestro --platform android test --config "$E2E_DIR/config.yaml" -e APP_ID=com.shopify.checkoutkit.androiddemo -e 'CART_BOOTSTRAP_LINK=checkout-kit-android://cart?productIndex=0&quantity=1' -e STOP_APP_BEFORE_DEEPLINK=false -e USE_IOS_PAYMENT_FIELDS=false -e USE_ANDROID_PAYMENT_FIELDS=true "$E2E_DIR/shared/checkout-smoke.yaml" diff --git a/platforms/react-native/package.json b/platforms/react-native/package.json index 941f287d..2ed7ac47 100644 --- a/platforms/react-native/package.json +++ b/platforms/react-native/package.json @@ -37,7 +37,9 @@ "pod-install": "bash ./scripts/pod_install", "snapshot": "./scripts/create_snapshot", "compare-snapshot": "./scripts/compare_snapshot", - "test": "jest" + "test": "jest", + "e2e:ios": "maestro --platform ios test --config ../../e2e/config.yaml -e APP_ID=com.shopify.checkoutkit.reactnativedemo -e 'CART_BOOTSTRAP_LINK=com.shopify.checkoutkit.reactnativedemo://cart?productIndex=0&quantity=1' -e STOP_APP_BEFORE_DEEPLINK=false -e USE_IOS_PAYMENT_FIELDS=true -e USE_ANDROID_PAYMENT_FIELDS=false ../../e2e/shared/checkout-smoke.yaml", + "e2e:android": "maestro --platform android test --config ../../e2e/config.yaml -e APP_ID=com.shopify.checkoutkit.reactnativedemo -e 'CART_BOOTSTRAP_LINK=com.shopify.checkoutkit.reactnativedemo://cart?productIndex=0&quantity=1' -e STOP_APP_BEFORE_DEEPLINK=true -e USE_IOS_PAYMENT_FIELDS=false -e USE_ANDROID_PAYMENT_FIELDS=true -e DISMISS_ADDRESS_KEYBOARD_AFTER_SUGGESTIONS=true ../../e2e/shared/checkout-smoke.yaml" }, "devDependencies": { "@babel/core": "^7.29.7", diff --git a/platforms/swift/Scripts/e2e_maestro_ios b/platforms/swift/Scripts/e2e_maestro_ios new file mode 100755 index 00000000..5e7e9caf --- /dev/null +++ b/platforms/swift/Scripts/e2e_maestro_ios @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +E2E_DIR="$(cd "$SCRIPT_DIR/../../../e2e" && pwd)" + +maestro --platform ios test --config "$E2E_DIR/config.yaml" -e APP_ID=com.shopify.checkoutkit.swiftdemo -e 'CART_BOOTSTRAP_LINK=checkout-kit-swift://cart?productIndex=0&quantity=1' -e STOP_APP_BEFORE_DEEPLINK=false -e USE_IOS_PAYMENT_FIELDS=true -e USE_ANDROID_PAYMENT_FIELDS=false "$E2E_DIR/shared/checkout-smoke.yaml"