diff --git a/e2e/README.md b/e2e/README.md index e744eb4c..b93ce9cd 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -51,7 +51,7 @@ terminal. | Platform | From | Command | | ------------------ | ------------------------------- | ------------------ | | React Native, iOS | `platforms/react-native/` | `pnpm e2e:ios` | -| Swift, iOS | TBD | TBD | +| Swift, iOS | `platforms/swift/` | `./Scripts/e2e_maestro_ios` | | Android (native) | TBD | TBD | | RN, Android | `platforms/react-native/` | `pnpm e2e:android` | diff --git a/e2e/swift/checkout-completion.yaml b/e2e/swift/checkout-completion.yaml new file mode 100644 index 00000000..dcf3eedd --- /dev/null +++ b/e2e/swift/checkout-completion.yaml @@ -0,0 +1,272 @@ +appId: com.shopify.example.MobileBuyIntegration +name: Checkout submits and shows result +tags: + - swift + - ios + - checkout + +env: + PRODUCT_INDEX: "1" + + # 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: "4242424242424242" + 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)" +- runFlow: + when: + visible: + id: catalog-tab + commands: + - tapOn: + id: catalog-tab +- extendedWaitUntil: + visible: + id: product-0-grid-item + timeout: 60000 +- scrollUntilVisible: + element: + id: product-${PRODUCT_INDEX}-grid-item + direction: DOWN + timeout: 5000 + centerElement: true +- tapOn: + id: product-${PRODUCT_INDEX}-grid-item +- extendedWaitUntil: + visible: + id: add-to-cart-button + timeout: 5000 +- tapOn: + id: add-to-cart-button + enabled: true +- extendedWaitUntil: + visible: "^Added$" + timeout: 15000 +- tapOn: + id: product-sheet-close-button +- waitForAnimationToEnd: + timeout: 3000 +- 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)?$" + index: -1 +- inputText: "maestro.e2e@shopify.com" +- tapOn: "selected" +- tapOn: + text: "^First name( \\(optional\\))?$" + index: -1 +- inputText: "Maestro" +- tapOn: "selected" +- tapOn: + text: "^Last name$" + index: -1 +- 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: "^Complete order$" + direction: DOWN + timeout: 5000 + centerElement: true + optional: true +- runFlow: + when: + notVisible: "^Complete order$" + commands: + - 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$" + direction: DOWN + timeout: 5000 + centerElement: true + optional: true + - runFlow: + when: + visible: "^Pay now$" + commands: + - tapOn: + text: "^Pay now$" + enabled: true +- runFlow: + when: + visible: "^Complete order$" + commands: + - tapOn: + text: "^Complete order$" + enabled: true +- extendedWaitUntil: + visible: "${POST_SUBMIT_RESULT_PATTERN}" + timeout: 60000 diff --git a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj index 2a81f732..7f779e48 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj +++ b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj @@ -159,7 +159,7 @@ mainGroup = 4EBBA75E2A5F0CE200193E19; packageReferences = ( CB00000012345678 /* XCRemoteSwiftPackageReference "apollo-ios" */, - CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */, + CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../.." */, ); productRefGroup = 4EBBA7682A5F0CE200193E19 /* Products */; projectDirPath = ""; @@ -443,9 +443,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */ = { + CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../.." */ = { isa = XCLocalSwiftPackageReference; - relativePath = "../../../../../checkout-kit"; + relativePath = "../../../.."; }; /* End XCLocalSwiftPackageReference section */ @@ -473,17 +473,17 @@ }; CB001E302F3CDA0300286F69 /* ShopifyCheckoutProtocol */ = { isa = XCSwiftPackageProductDependency; - package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */; + package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../.." */; productName = ShopifyCheckoutProtocol; }; CB1B10B42E4CDDB0001713F8 /* ShopifyCheckoutKit */ = { isa = XCSwiftPackageProductDependency; - package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */; + package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../.." */; productName = ShopifyCheckoutKit; }; CBED2D4E2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts */ = { isa = XCSwiftPackageProductDependency; - package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */; + package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../.." */; productName = ShopifyAcceleratedCheckouts; }; /* End XCSwiftPackageProductDependency section */ diff --git a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/App/SceneDelegate.swift b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/App/SceneDelegate.swift index cd67f3e6..d9bf0f4f 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/App/SceneDelegate.swift +++ b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/App/SceneDelegate.swift @@ -85,6 +85,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Catalog grid view productGridController.tabBarItem.image = UIImage(systemName: "square.grid.2x2") productGridController.tabBarItem.title = "Catalog" + productGridController.tabBarItem.accessibilityIdentifier = "catalog-tab" productGridController.navigationItem.titleView = logoImageView catalogCartButton = createCartButtonWithBadge() productGridController.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: catalogCartButton!) @@ -92,6 +93,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Product Gallery productGalleryController.tabBarItem.image = UIImage(systemName: "appwindow.swipe.rectangle") productGalleryController.tabBarItem.title = "Products" + productGalleryController.tabBarItem.accessibilityIdentifier = "products-tab" productGalleryController.navigationItem.titleView = logoImageView galleryCartButton = createCartButtonWithBadge() productGalleryController.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: galleryCartButton!) @@ -99,16 +101,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Cart (UI Kit) swiftUICartController.tabBarItem.image = UIImage(systemName: "cart") swiftUICartController.tabBarItem.title = "Cart" + swiftUICartController.tabBarItem.accessibilityIdentifier = "cart-tab" swiftUICartController.navigationItem.title = "Cart (SwiftUI)" // Account accountController.tabBarItem.image = UIImage(systemName: "person.circle") accountController.tabBarItem.title = "Log in" + accountController.tabBarItem.accessibilityIdentifier = "account-tab" subscribeToAuthStateChanges() // Settings settingsController.tabBarItem.image = UIImage(systemName: "gearshape.2") settingsController.tabBarItem.title = "Settings" + settingsController.tabBarItem.accessibilityIdentifier = "settings-tab" } private func subscribeToAuthStateChanges() { diff --git a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/Cart/CartView.swift b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/Cart/CartView.swift index 65b5e40c..06ae4fc9 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/Cart/CartView.swift +++ b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/Cart/CartView.swift @@ -80,7 +80,7 @@ struct CartView: View { ) .disabled(isBusy) .foregroundColor(.white) - .accessibilityIdentifier("checkoutSheetButton") + .accessibilityIdentifier("checkout-button") } .padding(.horizontal, 20) .padding(.bottom, 20) diff --git a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductGridView.swift b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductGridView.swift index 62af067d..708fd7d3 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductGridView.swift +++ b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductGridView.swift @@ -15,8 +15,9 @@ struct ProductGridView: View { ScrollView { LazyVGrid(columns: columns, spacing: 2) { if let products = productCache.collection, !products.isEmpty { - ForEach(products, id: \.id) { product in + ForEach(Array(products.enumerated()), id: \.element.id) { index, product in ProductGridItem(product: product) + .accessibilityIdentifier("product-\(index)-grid-item") .onTapGesture { selectProductAndShowSheet(for: product) } @@ -65,6 +66,7 @@ struct ProductSheetView: View { .padding() .foregroundStyle(.white) } + .accessibilityIdentifier("product-sheet-close-button") .padding([.top, .trailing], 16) } .edgesIgnoringSafeArea(.top) diff --git a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductView.swift b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductView.swift index bf8edd19..1c231bdf 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductView.swift +++ b/platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration/Sources/Scenes/ProductView.swift @@ -111,6 +111,7 @@ struct ProductView: View { .foregroundStyle(.white) .cornerRadius(DesignSystem.cornerRadius) .disabled(!variant.availableForSale || loading) + .accessibilityIdentifier("add-to-cart-button") if variant.availableForSale { AcceleratedCheckoutButtons(variantID: variant.id, quantity: 1) diff --git a/platforms/swift/Scripts/e2e_maestro_ios b/platforms/swift/Scripts/e2e_maestro_ios new file mode 100755 index 00000000..f640e8c5 --- /dev/null +++ b/platforms/swift/Scripts/e2e_maestro_ios @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +maestro --platform ios test --config ../../e2e/config.yaml ../../e2e/swift