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") } }