From 7efb7a6259a990224fb21dd1036355f86e2bde2f Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Thu, 30 Jul 2026 21:27:43 +0100 Subject: [PATCH] feat(e2e): add the hardcoded buyer identity order test on all four targets Row 3 of the test rollout. The cart carries the contact details and the delivery address, so checkout asks only for payment. That makes this the shortest real order, and the first test to look at when checkout breaks. The test moves from tests/react-native/ to tests/shared/, so Swift iOS, Kotlin Android, React Native iOS and React Native Android all run it. The matrix drops the `full` exclude entry that kept the ordering tests off the native rows. Android supplied only email, phone and country in its cart, while Swift and React Native both supplied a delivery address. Checkout therefore showed an empty Delivery form on Android only. CartInput.delivery now carries a pre-selected one-time address there too, which matches Swift. The address fields come from the same .env values the other samples read. Two Maestro details drive the flow changes: - A `visibilityPercentage` below 100 normalizes to zero, so `scrollUntilVisible` stops without scrolling. `centerElement: true` replaces it. A tap that lands on the keyboard does nothing, and Maestro still reports it as completed. - Android `hideKeyboard` issues a Back press when no keyboard is up, which closes the checkout sheet. dismiss-active-field.yaml therefore only ever follows an inputText. Verified green on all four targets, plus dev check, the Ruby suite, the Swift package and sample targets, the Android library and sample tests, the React Native JS and iOS tests, web and protocol. Co-Authored-By: Claude Opus 5 (1M context) Assisted-By: devx/ff2aa6e8-6c55-4f90-9e4e-f716ad36255d --- e2e/config/matrix.yml | 4 -- e2e/flows/checkout/assert-complete.yaml | 4 +- .../assert-returned-to-empty-cart.yaml | 8 ++- e2e/flows/checkout/dismiss-active-field.yaml | 9 +++ e2e/flows/checkout/fill-billing-address.yaml | 43 ++++++++++++ e2e/flows/checkout/fill-payment-card.yaml | 9 ++- e2e/flows/checkout/submit.yaml | 26 ++++++- e2e/test/checkout_payment_fixture_test.rb | 31 +++++++++ ...2e_matrix_to_browserstack_run_plan_test.rb | 4 +- e2e/tests/react-native/checkout-guest.yaml | 3 + .../checkout-hardcoded-buyer-identity.yaml | 24 ------- .../checkout-hardcoded-buyer-identity.yaml | 39 +++++++++++ .../CheckoutKitAndroidDemo/app/build.gradle | 18 +++++ .../checkoutkit/androiddemo/CheckoutKitApp.kt | 7 +- .../accessibility/AccessibilityIdentifiers.kt | 5 ++ .../checkoutkit/androiddemo/cart/CartView.kt | 1 + .../androiddemo/cart/data/CartRepository.kt | 67 +++++++++++++------ .../cart/data/DemoBuyerIdentity.kt | 35 +++++++++- .../AccessibilityIdentifiersTest.kt | 6 ++ .../cart/data/CartRepositoryTest.kt | 38 +++++++++++ .../cart/data/DemoBuyerIdentityTest.kt | 44 ++++++++++++ .../AccessibilityIdentifiers.swift | 5 ++ .../Sources/App/SceneDelegate.swift | 1 + .../Sources/Scenes/Cart/CartView.swift | 1 + .../AccessibilityIdentifiersTests.swift | 5 ++ 25 files changed, 377 insertions(+), 60 deletions(-) create mode 100644 e2e/flows/checkout/fill-billing-address.yaml create mode 100644 e2e/test/checkout_payment_fixture_test.rb delete mode 100644 e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml create mode 100644 e2e/tests/shared/checkout-hardcoded-buyer-identity.yaml create mode 100644 platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepositoryTest.kt create mode 100644 platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentityTest.kt diff --git a/e2e/config/matrix.yml b/e2e/config/matrix.yml index c342e7d8d..edcc7605b 100644 --- a/e2e/config/matrix.yml +++ b/e2e/config/matrix.yml @@ -10,10 +10,6 @@ tags: exclude: - flaky - wip - # The remaining `full` tests place a real order and live under tests/react-native/, - # so they cannot run on the Swift and Kotlin rows. Drop this entry when the shared - # ordering tests replace them. - - full applications: - id: react-native-ios target: react-native diff --git a/e2e/flows/checkout/assert-complete.yaml b/e2e/flows/checkout/assert-complete.yaml index 3269468a0..b91f2028b 100644 --- a/e2e/flows/checkout/assert-complete.yaml +++ b/e2e/flows/checkout/assert-complete.yaml @@ -1,5 +1,7 @@ appId: ${E2E_APP_ID} --- +# The payment provider and the order creation both run before the page changes, so this +# wait covers two remote systems rather than a render. - extendedWaitUntil: visible: "${POST_SUBMIT_RESULT_PATTERN}" - timeout: 10000 + timeout: 60000 diff --git a/e2e/flows/checkout/assert-returned-to-empty-cart.yaml b/e2e/flows/checkout/assert-returned-to-empty-cart.yaml index 34ef280a4..c5005b9ea 100644 --- a/e2e/flows/checkout/assert-returned-to-empty-cart.yaml +++ b/e2e/flows/checkout/assert-returned-to-empty-cart.yaml @@ -2,10 +2,14 @@ appId: ${E2E_APP_ID} --- - runFlow: close.yaml +# The four samples return to four different screens after checkout closes, so the flow +# asserts nothing about which screen that is. The cart control belongs to the app chrome +# and stays on screen everywhere, which makes it both the "we are back" signal and the +# way to the cart. - extendedWaitUntil: visible: - id: catalog-tab - timeout: 10000 + id: cart-tab + timeout: 30000 - tapOn: id: cart-tab diff --git a/e2e/flows/checkout/dismiss-active-field.yaml b/e2e/flows/checkout/dismiss-active-field.yaml index 9407fd75d..40b8c6f90 100644 --- a/e2e/flows/checkout/dismiss-active-field.yaml +++ b/e2e/flows/checkout/dismiss-active-field.yaml @@ -5,6 +5,15 @@ appId: ${E2E_APP_ID} platform: android commands: - hideKeyboard +# iOS keeps the keyboard above the page until the accessory bar closes it. The next field +# then stays behind the keyboard, and the tap that follows lands on a key instead. +- runFlow: + when: + platform: ios + visible: "Done" + commands: + - tapOn: "Done" + - waitForAnimationToEnd - runFlow: when: platform: ios diff --git a/e2e/flows/checkout/fill-billing-address.yaml b/e2e/flows/checkout/fill-billing-address.yaml new file mode 100644 index 000000000..8e7ffc9a9 --- /dev/null +++ b/e2e/flows/checkout/fill-billing-address.yaml @@ -0,0 +1,43 @@ +appId: ${E2E_APP_ID} +--- +# Checkout offers a "use the shipping address" control only when it can copy the address +# the cart supplied. The samples build their carts through different mutations, so one +# target gets the control and another gets an empty billing form. The scroll below stops +# at whichever the target shows, and the branch fills the form only when there is one. +- scrollUntilVisible: + element: + text: "^(Billing address|Use shipping address as billing address)$" + direction: DOWN + centerElement: true +- runFlow: + when: + visible: "^Billing address$" + commands: + # Country/Region and Province already hold the shop defaults, so this flow leaves + # them alone and supplies an address that suits those defaults. + - scrollUntilVisible: + element: + text: "^First name( \\(optional\\))?$" + direction: DOWN + centerElement: true + - tapOn: + text: "^First name( \\(optional\\))?$" + - inputText: "${BILLING_FIRST_NAME}" + - runFlow: dismiss-active-field.yaml + - tapOn: + text: "^Last name$" + - inputText: "${BILLING_LAST_NAME}" + - runFlow: dismiss-active-field.yaml + - tapOn: + text: "^Address$" + - inputText: "${BILLING_ADDRESS_LINE1}" + - runFlow: dismiss-active-field.yaml + - tapOn: + text: "^City$" + - inputText: "${BILLING_CITY}" + - runFlow: dismiss-active-field.yaml + - tapOn: + text: "^Postal code$" + - inputText: "${BILLING_POSTAL_CODE}" + - runFlow: dismiss-active-field.yaml + - waitForAnimationToEnd diff --git a/e2e/flows/checkout/fill-payment-card.yaml b/e2e/flows/checkout/fill-payment-card.yaml index bee1b29f8..0504c8515 100644 --- a/e2e/flows/checkout/fill-payment-card.yaml +++ b/e2e/flows/checkout/fill-payment-card.yaml @@ -9,8 +9,9 @@ appId: ${E2E_APP_ID} text: "^Card number$" - waitForAnimationToEnd - inputText: "${CARD_NUMBER}" +# Checkout groups the digits, so the field never reads back the value the test typed. - extendedWaitUntil: - visible: "^${CARD_NUMBER}$" + visible: "^${CARD_NUMBER_DISPLAY}$" - runFlow: dismiss-active-field.yaml - scrollUntilVisible: element: @@ -31,4 +32,10 @@ appId: ${E2E_APP_ID} - waitForAnimationToEnd - inputText: "${CARD_SECURITY_CODE}" - runFlow: dismiss-active-field.yaml +- tapOn: + text: "^Name on card$" +- waitForAnimationToEnd +- eraseText +- inputText: "${CARD_HOLDER_NAME}" +- runFlow: dismiss-active-field.yaml - waitForAnimationToEnd diff --git a/e2e/flows/checkout/submit.yaml b/e2e/flows/checkout/submit.yaml index 56e90d6b1..0b1472453 100644 --- a/e2e/flows/checkout/submit.yaml +++ b/e2e/flows/checkout/submit.yaml @@ -1,10 +1,32 @@ appId: ${E2E_APP_ID} --- +# The keyboard from the last payment field can still cover the foot of the page, so the +# control has to reach the middle of the screen before the tap. `centerElement` does that. +# A `visibilityPercentage` below 100 rounds down to zero, which lets the scroll stop early, +# and a tap that lands on the keyboard does nothing while Maestro still reports it as done. - scrollUntilVisible: element: - text: "^(Pay now|Complete order)$" + text: "^(Pay now|Complete order|Review order)$" direction: DOWN - visibilityPercentage: 90 + centerElement: true + +# A shop that shows shipping options keeps the pay action behind a review step. One button +# carries both labels, so the review tap comes first and the pay tap follows it. +- runFlow: + when: + visible: "^Review order$" + commands: + - tapOn: + text: "^Review order$" + enabled: true + - extendedWaitUntil: + visible: "^(Pay now|Complete order)$" + timeout: 30000 + - scrollUntilVisible: + element: + text: "^(Pay now|Complete order)$" + direction: DOWN + centerElement: true - tapOn: text: "^(Pay now|Complete order)$" enabled: true diff --git a/e2e/test/checkout_payment_fixture_test.rb b/e2e/test/checkout_payment_fixture_test.rb new file mode 100644 index 000000000..63f30f751 --- /dev/null +++ b/e2e/test/checkout_payment_fixture_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "yaml" + +class CheckoutPaymentFixtureTest < Minitest::Test + E2E_ROOT = File.expand_path("..", __dir__) + CHECKOUT_PATH = File.join(E2E_ROOT, "tests", "shared", "checkout-hardcoded-buyer-identity.yaml") + PAYMENT_FLOW_PATH = File.join(E2E_ROOT, "flows", "checkout", "fill-payment-card.yaml") + + def yaml_documents(path) + File.read(path).split(/^---\s*$/).map { |document| YAML.safe_load(document) } + end + + def test_successful_checkout_uses_the_bogus_gateway_approval_number + checkout = yaml_documents(CHECKOUT_PATH).first + + assert_equal "1", checkout.fetch("env").fetch("CARD_NUMBER") + assert_equal "1", checkout.fetch("env").fetch("CARD_NUMBER_DISPLAY") + end + + def test_payment_flow_replaces_the_prefilled_cardholder_name + commands = yaml_documents(PAYMENT_FLOW_PATH).last + name_field_index = commands.index { |command| command.is_a?(Hash) && command.dig("tapOn", "text") == "^Name on card$" } + erase_index = commands.index("eraseText") + cardholder_name_index = commands.index { |command| command == {"inputText" => "${CARD_HOLDER_NAME}"} } + + assert_equal name_field_index + 2, erase_index + assert_equal erase_index + 1, cardholder_name_index + end +end diff --git a/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb b/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb index 99228a2b8..1c928cbe5 100644 --- a/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb +++ b/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb @@ -48,8 +48,8 @@ def test_runs_carry_default_tags_and_the_other_platform_exclusion android_run = run_for("kotlin-android") assert_equal ["launch", "checkout"], ios_run.fetch("include_tags") - assert_equal ["flaky", "wip", "full", "android-only"], ios_run.fetch("exclude_tags") - assert_equal ["flaky", "wip", "full", "ios-only"], android_run.fetch("exclude_tags") + assert_equal ["flaky", "wip", "android-only"], ios_run.fetch("exclude_tags") + assert_equal ["flaky", "wip", "ios-only"], android_run.fetch("exclude_tags") end def test_an_application_overrides_the_default_tags diff --git a/e2e/tests/react-native/checkout-guest.yaml b/e2e/tests/react-native/checkout-guest.yaml index d98b3e055..958dfcafc 100644 --- a/e2e/tests/react-native/checkout-guest.yaml +++ b/e2e/tests/react-native/checkout-guest.yaml @@ -3,6 +3,9 @@ name: React Native checkout - guest tags: - checkout - full + # Only React Native runs this today, and every row now runs the whole tests folder. + # The shared guest test replaces this file, and this tag goes with it. + - wip env: # Sample app buyer identity configuration diff --git a/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml b/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml deleted file mode 100644 index 47215670e..000000000 --- a/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${E2E_APP_ID} -name: React Native checkout - hardcoded buyer identity -tags: - - checkout - # This test submits a real order, so it belongs to the expensive tier. - - full - -env: - # Sample app buyer identity configuration - E2E_CART_PARAMS: "productIndex=0&quantity=1&buyerIdentityMode=hardcoded" - - # Checkout payment fixture - CARD_NUMBER: "1" - CARD_SECURITY_CODE: "123" - - # Accepted successful checkout states for this smoke test. - POST_SUBMIT_RESULT_PATTERN: ".*(Thank you|Your order|Order confirmed|confirmation).*" ---- -- runFlow: ../../flows/app/bootstrap-cart-from-link.yaml -- runFlow: ../../flows/checkout/present.yaml -- runFlow: ../../flows/checkout/fill-payment-card.yaml -- runFlow: ../../flows/checkout/submit.yaml -- runFlow: ../../flows/checkout/assert-complete.yaml -- runFlow: ../../flows/checkout/assert-returned-to-empty-cart.yaml diff --git a/e2e/tests/shared/checkout-hardcoded-buyer-identity.yaml b/e2e/tests/shared/checkout-hardcoded-buyer-identity.yaml new file mode 100644 index 000000000..fb4719ace --- /dev/null +++ b/e2e/tests/shared/checkout-hardcoded-buyer-identity.yaml @@ -0,0 +1,39 @@ +appId: ${E2E_APP_ID} +name: Checkout with hardcoded buyer identity +tags: + - checkout + # This test submits a real order, so it belongs to the expensive tier. + - full + +env: + # The cart carries the contact and the delivery address, so checkout asks for payment + # only. That makes this the shortest real order, and the first one to fix when checkout + # breaks. + E2E_CART_PARAMS: "productIndex=0&quantity=1&buyerIdentityMode=hardcoded" + + # Checkout payment fixture. The shop runs the bogus gateway, which rejects real card + # brands and reads a single digit instead: 1 approves, 2 declines, 3 fails the gateway. + CARD_NUMBER: "1" + CARD_NUMBER_DISPLAY: "1" + CARD_SECURITY_CODE: "123" + CARD_HOLDER_NAME: "Maestro Shopify" + + # Checkout billing fixture. The cart supplies a one-time delivery address, which the + # billing section cannot copy, so the test types a billing address of its own. Country + # and province keep the shop defaults, so this address stays in Canada. + BILLING_FIRST_NAME: "Maestro" + BILLING_LAST_NAME: "Shopify" + BILLING_ADDRESS_LINE1: "620 King Street West" + BILLING_CITY: "Toronto" + BILLING_POSTAL_CODE: "M5V 1M7" + + # Accepted successful checkout states for this smoke test. + POST_SUBMIT_RESULT_PATTERN: ".*(Thank you|Your order|Order confirmed|confirmation).*" +--- +- runFlow: ../../flows/app/bootstrap-cart-from-link.yaml +- runFlow: ../../flows/checkout/present.yaml +- runFlow: ../../flows/checkout/fill-payment-card.yaml +- runFlow: ../../flows/checkout/fill-billing-address.yaml +- runFlow: ../../flows/checkout/submit.yaml +- runFlow: ../../flows/checkout/assert-complete.yaml +- runFlow: ../../flows/checkout/assert-returned-to-empty-cart.yaml diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/build.gradle b/platforms/android/samples/CheckoutKitAndroidDemo/app/build.gradle index d9a6e1b26..b2eded034 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/build.gradle +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/build.gradle @@ -59,6 +59,15 @@ def customerAccountRedirectHost = customerAccountRedirect?.host ?: "callback" // Demo buyer identity (prefill toggle in Settings) def prefillEmail = properties.getProperty("EMAIL", properties.getProperty("PREFILL_EMAIL", "test.buyer@example.com")) def prefillPhone = properties.getProperty("PHONE", properties.getProperty("PREFILL_PHONE", "+14165550100")) +def prefillFirstName = propertyOrDefault("FIRST_NAME", "Evelyn") +def prefillLastName = propertyOrDefault("LAST_NAME", "Hartley") +def prefillAddress1 = propertyOrDefault("ADDRESS_1", "650 King Street") +def prefillAddress2 = propertyOrDefault("ADDRESS_2", "Shopify HQ") +def prefillCompany = propertyOrDefault("COMPANY", "Shopify") +def prefillCity = propertyOrDefault("CITY", "Toronto") +def prefillProvince = propertyOrDefault("PROVINCE", "ON") +def prefillZip = propertyOrDefault("ZIP", "M5V 1M7") +def prefillCountry = propertyOrDefault("COUNTRY", "CA") if (!storefrontDomain || !accessToken) { println("**** Please add a .env file with STOREFRONT_DOMAIN and STOREFRONT_ACCESS_TOKEN set *****") @@ -93,6 +102,15 @@ android { buildConfigField "String", "customerAccountApiGraphQLBaseUrl", "\"$customerAccountApiGraphQLBaseUrl\"" buildConfigField "String", "prefillEmail", "\"$prefillEmail\"" buildConfigField "String", "prefillPhone", "\"$prefillPhone\"" + buildConfigField "String", "prefillFirstName", "\"$prefillFirstName\"" + buildConfigField "String", "prefillLastName", "\"$prefillLastName\"" + buildConfigField "String", "prefillAddress1", "\"$prefillAddress1\"" + buildConfigField "String", "prefillAddress2", "\"$prefillAddress2\"" + buildConfigField "String", "prefillCompany", "\"$prefillCompany\"" + buildConfigField "String", "prefillCity", "\"$prefillCity\"" + buildConfigField "String", "prefillProvince", "\"$prefillProvince\"" + buildConfigField "String", "prefillZip", "\"$prefillZip\"" + buildConfigField "String", "prefillCountry", "\"$prefillCountry\"" } signingConfigs { diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/CheckoutKitApp.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/CheckoutKitApp.kt index 4f70b9c3b..19093e4f5 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/CheckoutKitApp.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/CheckoutKitApp.kt @@ -146,9 +146,10 @@ fun CheckoutKitAppRoot( ) }, actions = { - IconButton(onClick = { - navController.navigate(Screen.Cart.route) - }) { + IconButton( + onClick = { navController.navigate(Screen.Cart.route) }, + modifier = Modifier.testTag(AccessibilityIdentifiers.Tabs.CART), + ) { BadgedBox(badge = { if (totalQuantity > 0) { Badge( diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiers.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiers.kt index 5f95af6bf..d94343dfa 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiers.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiers.kt @@ -6,5 +6,10 @@ object AccessibilityIdentifiers { object Cart { const val CHECKOUT_READY = "cart-checkout-ready" const val CHECKOUT_BUTTON = "checkout-button" + const val EMPTY_MESSAGE = "cart-empty-message" + } + + object Tabs { + const val CART = "cart-tab" } } diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartView.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartView.kt index c36df4c9b..71e8d0dfc 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartView.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartView.kt @@ -277,6 +277,7 @@ private fun EmptyCartMessage( ) { Header2( text = stringResource(id = R.string.cart_empty), + modifier = Modifier.testTag(AccessibilityIdentifiers.Cart.EMPTY_MESSAGE), ) BodyMedium( stringResource(id = R.string.cart_emtpy_description), diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepository.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepository.kt index add07b5e4..c14dcb810 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepository.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepository.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo.api.Optional import com.shopify.checkoutkit.androiddemo.common.ID import com.shopify.checkoutkit.androiddemo.common.client.StorefrontApiClient import com.shopify.checkoutkit.androiddemo.graphql.type.CartBuyerIdentityInput +import com.shopify.checkoutkit.androiddemo.graphql.type.CartDeliveryInput import com.shopify.checkoutkit.androiddemo.graphql.type.CartInput import com.shopify.checkoutkit.androiddemo.graphql.type.CartLineInput import com.shopify.checkoutkit.androiddemo.graphql.type.CartLineUpdateInput @@ -20,17 +21,7 @@ class CartRepository( demoBuyerIdentityEnabled: Boolean, customerAccessToken: String?, ): CartState.Cart { - val input = CartInput( - lines = Optional.present( - listOf( - CartLineInput( - merchandiseId = variantId.id, - quantity = Optional.present(quantity), - ) - ) - ), - buyerIdentity = Optional.present(buyerIdentity(demoBuyerIdentityEnabled, customerAccessToken)), - ) + val input = cartInput(variantId, quantity, demoBuyerIdentityEnabled, customerAccessToken) val data = storefrontApiClient.createCart(input) val cartCreate = data.cartCreate @@ -76,17 +67,53 @@ class CartRepository( } } - private fun buyerIdentity(demoBuyerIdentityEnabled: Boolean, customerAccessToken: String?): CartBuyerIdentityInput { - if (customerAccessToken != null) { - Timber.i("Setting a customer access token in buyer identity") - return CartBuyerIdentityInput(customerAccessToken = Optional.present(customerAccessToken)) + companion object { + internal fun cartInput( + variantId: ID, + quantity: Int, + demoBuyerIdentityEnabled: Boolean, + customerAccessToken: String?, + ) = CartInput( + lines = Optional.present( + listOf( + CartLineInput( + merchandiseId = variantId.id, + quantity = Optional.present(quantity), + ) + ) + ), + buyerIdentity = Optional.present(buyerIdentity(demoBuyerIdentityEnabled, customerAccessToken)), + delivery = delivery(demoBuyerIdentityEnabled, customerAccessToken), + ) + + private fun buyerIdentity( + demoBuyerIdentityEnabled: Boolean, + customerAccessToken: String?, + ): CartBuyerIdentityInput { + if (customerAccessToken != null) { + Timber.i("Setting a customer access token in buyer identity") + return CartBuyerIdentityInput(customerAccessToken = Optional.present(customerAccessToken)) + } + + return if (demoBuyerIdentityEnabled) { + Timber.i("Using demo buyer identity data to prefill checkout") + DemoBuyerIdentity.value + } else { + CartBuyerIdentityInput(countryCode = Optional.present(CountryCode.CA)) + } } - return if (demoBuyerIdentityEnabled) { - Timber.i("Using demo buyer identity data to prefill checkout") - DemoBuyerIdentity.value - } else { - CartBuyerIdentityInput(countryCode = Optional.present(CountryCode.CA)) + // A signed in customer picks from the addresses the account already holds, so only the + // demo identity carries one of its own. + private fun delivery( + demoBuyerIdentityEnabled: Boolean, + customerAccessToken: String?, + ): Optional { + if (customerAccessToken != null || !demoBuyerIdentityEnabled) { + return Optional.Absent + } + + return Optional.present(DemoBuyerIdentity.delivery) } } } diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentity.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentity.kt index 07c9b5f01..044be913e 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentity.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentity.kt @@ -2,13 +2,46 @@ package com.shopify.checkoutkit.androiddemo.cart.data import com.apollographql.apollo.api.Optional import com.shopify.checkoutkit.androiddemo.BuildConfig +import com.shopify.checkoutkit.androiddemo.graphql.type.CartAddressInput import com.shopify.checkoutkit.androiddemo.graphql.type.CartBuyerIdentityInput +import com.shopify.checkoutkit.androiddemo.graphql.type.CartDeliveryAddressInput +import com.shopify.checkoutkit.androiddemo.graphql.type.CartDeliveryInput +import com.shopify.checkoutkit.androiddemo.graphql.type.CartSelectableAddressInput import com.shopify.checkoutkit.androiddemo.graphql.type.CountryCode object DemoBuyerIdentity { + private val countryCode = CountryCode.safeValueOf(BuildConfig.prefillCountry) + internal val value = CartBuyerIdentityInput( email = Optional.present(BuildConfig.prefillEmail), - countryCode = Optional.present(CountryCode.CA), + countryCode = Optional.present(countryCode), phone = Optional.present(BuildConfig.prefillPhone), ) + + internal val delivery = CartDeliveryInput( + addresses = Optional.present( + listOf( + CartSelectableAddressInput( + address = CartAddressInput( + deliveryAddress = Optional.present( + CartDeliveryAddressInput( + address1 = Optional.present(BuildConfig.prefillAddress1), + address2 = Optional.present(BuildConfig.prefillAddress2), + city = Optional.present(BuildConfig.prefillCity), + company = Optional.present(BuildConfig.prefillCompany), + countryCode = Optional.present(countryCode), + firstName = Optional.present(BuildConfig.prefillFirstName), + lastName = Optional.present(BuildConfig.prefillLastName), + phone = Optional.present(BuildConfig.prefillPhone), + provinceCode = Optional.present(BuildConfig.prefillProvince), + zip = Optional.present(BuildConfig.prefillZip), + ) + ) + ), + selected = Optional.present(true), + oneTimeUse = Optional.present(true), + ) + ) + ) + ) } diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiersTest.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiersTest.kt index 511a97005..1d650a1ce 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiersTest.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/accessibility/AccessibilityIdentifiersTest.kt @@ -13,5 +13,11 @@ class AccessibilityIdentifiersTest { fun `cart markers match the maestro flows`() { assertThat(AccessibilityIdentifiers.Cart.CHECKOUT_READY).isEqualTo("cart-checkout-ready") assertThat(AccessibilityIdentifiers.Cart.CHECKOUT_BUTTON).isEqualTo("checkout-button") + assertThat(AccessibilityIdentifiers.Cart.EMPTY_MESSAGE).isEqualTo("cart-empty-message") + } + + @Test + fun `tab markers match the maestro flows`() { + assertThat(AccessibilityIdentifiers.Tabs.CART).isEqualTo("cart-tab") } } diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepositoryTest.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepositoryTest.kt new file mode 100644 index 000000000..4b0645047 --- /dev/null +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/CartRepositoryTest.kt @@ -0,0 +1,38 @@ +package com.shopify.checkoutkit.androiddemo.cart.data + +import com.shopify.checkoutkit.androiddemo.common.ID +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class CartRepositoryTest { + + @Test + fun `demo buyer identity supplies the delivery address`() { + val input = cartInput(demoBuyerIdentityEnabled = true, customerAccessToken = null) + + assertThat(input.delivery.getOrThrow()).isEqualTo(DemoBuyerIdentity.delivery) + assertThat(input.buyerIdentity.getOrThrow()).isEqualTo(DemoBuyerIdentity.value) + } + + @Test + fun `a guest supplies no delivery address`() { + val input = cartInput(demoBuyerIdentityEnabled = false, customerAccessToken = null) + + assertThat(input.delivery.getOrNull()).isNull() + } + + @Test + fun `a signed in customer supplies no delivery address`() { + val input = cartInput(demoBuyerIdentityEnabled = true, customerAccessToken = "token") + + assertThat(input.delivery.getOrNull()).isNull() + } + + private fun cartInput(demoBuyerIdentityEnabled: Boolean, customerAccessToken: String?) = + CartRepository.cartInput( + variantId = ID("gid://shopify/ProductVariant/1"), + quantity = 1, + demoBuyerIdentityEnabled = demoBuyerIdentityEnabled, + customerAccessToken = customerAccessToken, + ) +} diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentityTest.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentityTest.kt new file mode 100644 index 000000000..df60dcfc8 --- /dev/null +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/cart/data/DemoBuyerIdentityTest.kt @@ -0,0 +1,44 @@ +package com.shopify.checkoutkit.androiddemo.cart.data + +import com.shopify.checkoutkit.androiddemo.BuildConfig +import com.shopify.checkoutkit.androiddemo.graphql.type.CountryCode +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class DemoBuyerIdentityTest { + + @Test + fun `buyer identity carries the contact details`() { + val identity = DemoBuyerIdentity.value + + assertThat(identity.email.getOrThrow()).isEqualTo(BuildConfig.prefillEmail) + assertThat(identity.phone.getOrThrow()).isEqualTo(BuildConfig.prefillPhone) + assertThat(identity.countryCode.getOrThrow()).isEqualTo(CountryCode.safeValueOf(BuildConfig.prefillCountry)) + } + + @Test + fun `delivery carries one pre-selected one-time address`() { + val addresses = DemoBuyerIdentity.delivery.addresses.getOrThrow()!! + + assertThat(addresses).hasSize(1) + assertThat(addresses.first().selected.getOrThrow()).isTrue() + assertThat(addresses.first().oneTimeUse.getOrThrow()).isTrue() + } + + @Test + fun `delivery address carries every field checkout asks a guest for`() { + val address = DemoBuyerIdentity.delivery.addresses.getOrThrow()!! + .first().address.deliveryAddress.getOrThrow()!! + + assertThat(address.firstName.getOrThrow()).isEqualTo(BuildConfig.prefillFirstName) + assertThat(address.lastName.getOrThrow()).isEqualTo(BuildConfig.prefillLastName) + assertThat(address.address1.getOrThrow()).isEqualTo(BuildConfig.prefillAddress1) + assertThat(address.address2.getOrThrow()).isEqualTo(BuildConfig.prefillAddress2) + assertThat(address.company.getOrThrow()).isEqualTo(BuildConfig.prefillCompany) + assertThat(address.city.getOrThrow()).isEqualTo(BuildConfig.prefillCity) + assertThat(address.provinceCode.getOrThrow()).isEqualTo(BuildConfig.prefillProvince) + assertThat(address.zip.getOrThrow()).isEqualTo(BuildConfig.prefillZip) + assertThat(address.phone.getOrThrow()).isEqualTo(BuildConfig.prefillPhone) + assertThat(address.countryCode.getOrThrow()).isEqualTo(CountryCode.safeValueOf(BuildConfig.prefillCountry)) + } +} diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Accessibility/AccessibilityIdentifiers.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Accessibility/AccessibilityIdentifiers.swift index 295073f23..fd3f2a42a 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Accessibility/AccessibilityIdentifiers.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Accessibility/AccessibilityIdentifiers.swift @@ -4,5 +4,10 @@ enum AccessibilityIdentifiers { enum Cart { static let checkoutReady = "cart-checkout-ready" static let checkoutButton = "checkout-button" + static let emptyMessage = "cart-empty-message" + } + + enum Tabs { + static let cart = "cart-tab" } } diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift index e597278a5..e5ffbfb19 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift @@ -101,6 +101,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Cart (UI Kit) swiftUICartController.tabBarItem.image = UIImage(systemName: "cart") swiftUICartController.tabBarItem.title = "Cart" + swiftUICartController.tabBarItem.accessibilityIdentifier = AccessibilityIdentifiers.Tabs.cart swiftUICartController.navigationItem.title = "Cart (SwiftUI)" // Account diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift index b7bda1d3a..46dadcfec 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift @@ -175,6 +175,7 @@ struct EmptyState: View { .padding(.bottom, 6) Text("Your cart is empty.") .font(.caption) + .accessibilityIdentifier(AccessibilityIdentifiers.Cart.emptyMessage) } } } diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/Accessibility/AccessibilityIdentifiersTests.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/Accessibility/AccessibilityIdentifiersTests.swift index 52c6e4373..6fc5b5cf4 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/Accessibility/AccessibilityIdentifiersTests.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/Accessibility/AccessibilityIdentifiersTests.swift @@ -9,5 +9,10 @@ class AccessibilityIdentifiersTests: XCTestCase { func testCartMarkersMatchTheMaestroFlows() { XCTAssertEqual(AccessibilityIdentifiers.Cart.checkoutReady, "cart-checkout-ready") XCTAssertEqual(AccessibilityIdentifiers.Cart.checkoutButton, "checkout-button") + XCTAssertEqual(AccessibilityIdentifiers.Cart.emptyMessage, "cart-empty-message") + } + + func testTabMarkersMatchTheMaestroFlows() { + XCTAssertEqual(AccessibilityIdentifiers.Tabs.cart, "cart-tab") } }