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 f95f13033..4650882d1 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 @@ -62,6 +62,7 @@ fun CartView( val state = cartViewModel.cartState.collectAsState().value val loading = cartViewModel.loadingState.collectAsState().value val checkoutPresentationMode = cartViewModel.checkoutPresentationMode.collectAsState().value + val preloadStateTestId = cartViewModel.preloadStateTestId.collectAsState().value val activity = LocalActivity.current as ComponentActivity var mutableQuantity by remember { mutableStateOf>(mutableMapOf()) } @@ -90,7 +91,12 @@ fun CartView( it.title to it.quantity } - Column(modifier = Modifier.padding(top = 4.dp)) { + // Exposes the current preload state as a preload identifier. + Column( + modifier = Modifier + .padding(top = 4.dp) + .testTag(preloadStateTestId) + ) { CartLines( lines = state.cartLines, loading = loading, diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt index d8d3d7d62..b2d23fbd6 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/cart/CartViewModel.kt @@ -11,6 +11,7 @@ import com.shopify.checkoutkit.CheckoutErrorCode import com.shopify.checkoutkit.CheckoutException import com.shopify.checkoutkit.CheckoutPresentation import com.shopify.checkoutkit.CheckoutProtocol +import com.shopify.checkoutkit.PreloadState import com.shopify.checkoutkit.ShopifyCheckoutKit import com.shopify.checkoutkit.androiddemo.MainActivity import com.shopify.checkoutkit.androiddemo.R @@ -22,6 +23,7 @@ import com.shopify.checkoutkit.androiddemo.common.SnackbarEvent import com.shopify.checkoutkit.androiddemo.common.logs.LogLevel import com.shopify.checkoutkit.androiddemo.common.logs.Logger import com.shopify.checkoutkit.androiddemo.common.navigation.Screen +import com.shopify.checkoutkit.androiddemo.e2e.PreloadStateMarker import com.shopify.checkoutkit.androiddemo.settings.PreferencesManager import com.shopify.checkoutkit.androiddemo.settings.authentication.data.AuthenticationState import com.shopify.checkoutkit.androiddemo.settings.authentication.data.CustomerRepository @@ -60,6 +62,9 @@ class CartViewModel( private val _checkoutPresentationMode = MutableStateFlow(CheckoutPresentationMode.CheckoutKitSheet) val checkoutPresentationMode: StateFlow = _checkoutPresentationMode.asStateFlow() + private val _preloadStateTestId = MutableStateFlow(PreloadStateMarker.testId(PreloadState.Idle)) + val preloadStateTestId: StateFlow = _preloadStateTestId.asStateFlow() + private var demoBuyerIdentityEnabled = false private var checkoutPreloadingEnabled = true private var windowOpenHandler = WindowOpenHandler.Default @@ -171,6 +176,7 @@ class CartViewModel( Timber.i("Preloading checkout") ShopifyCheckoutKit.preload(url, activity) { state -> Timber.i("Preload state changed to $state") + _preloadStateTestId.value = PreloadStateMarker.testId(state) } } diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIds.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIds.kt index 2b1b8686b..3464b9c85 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIds.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIds.kt @@ -2,6 +2,7 @@ package com.shopify.checkoutkit.androiddemo.e2e object E2ETestIds { const val APP_READY = "checkout-kit-sample-ready" + const val PRELOAD_STATE_PREFIX = "preload-state-" object Cart { const val CHECKOUT_READY = "cart-checkout-ready" diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/PreloadStateMarker.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/PreloadStateMarker.kt new file mode 100644 index 000000000..26b55aaae --- /dev/null +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/main/java/com/shopify/checkoutkit/androiddemo/e2e/PreloadStateMarker.kt @@ -0,0 +1,23 @@ +package com.shopify.checkoutkit.androiddemo.e2e + +import com.shopify.checkoutkit.PreloadState + +/** Maps [PreloadState] to a preload identifier. */ +object PreloadStateMarker { + fun testId(state: PreloadState): String = "${E2ETestIds.PRELOAD_STATE_PREFIX}${text(state)}" + + fun text(state: PreloadState): String = when (state) { + is PreloadState.Idle -> "idle" + is PreloadState.Loading -> "loading" + is PreloadState.Ready -> "ready" + is PreloadState.Expired -> "expired" + is PreloadState.Failed -> failedText(state.reason) + } + + private fun failedText(reason: PreloadState.FailureReason): String = when (reason) { + is PreloadState.FailureReason.HttpError -> "failed-http-${reason.statusCode}" + is PreloadState.FailureReason.NavigationFailed -> "failed-navigation" + is PreloadState.FailureReason.WebContentUnavailable -> "failed-web-content-unavailable" + is PreloadState.FailureReason.ProtocolError -> "failed-protocol" + } +} diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/PreloadStateMarkerTest.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/PreloadStateMarkerTest.kt new file mode 100644 index 000000000..e5ccb0da8 --- /dev/null +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/PreloadStateMarkerTest.kt @@ -0,0 +1,48 @@ +package com.shopify.checkoutkit.androiddemo.e2e + +import com.shopify.checkoutkit.PreloadState +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class PreloadStateMarkerTest { + @Test + fun `lifecycle marker texts match the maestro flow assertions`() { + assertThat(PreloadStateMarker.text(PreloadState.Idle)).isEqualTo("idle") + assertThat(PreloadStateMarker.text(PreloadState.Loading)).isEqualTo("loading") + assertThat(PreloadStateMarker.text(PreloadState.Ready)).isEqualTo("ready") + assertThat(PreloadStateMarker.text(PreloadState.Expired)).isEqualTo("expired") + } + + @Test + fun `http failure marker text includes the status code`() { + assertThat(markerFor(PreloadState.FailureReason.HttpError(statusCode = 403))) + .isEqualTo("failed-http-403") + assertThat(markerFor(PreloadState.FailureReason.HttpError(statusCode = 500))) + .isEqualTo("failed-http-500") + } + + @Test + fun `non-http failure marker texts`() { + assertThat(markerFor(PreloadState.FailureReason.NavigationFailed)).isEqualTo("failed-navigation") + assertThat(markerFor(PreloadState.FailureReason.WebContentUnavailable)) + .isEqualTo("failed-web-content-unavailable") + assertThat(markerFor(PreloadState.FailureReason.ProtocolError)).isEqualTo("failed-protocol") + } + + @Test + fun `dynamic test ids match the maestro flow assertions`() { + assertThat(PreloadStateMarker.testId(PreloadState.Idle)).isEqualTo("preload-state-idle") + assertThat(PreloadStateMarker.testId(PreloadState.Ready)).isEqualTo("preload-state-ready") + assertThat( + PreloadStateMarker.testId( + PreloadState.Failed( + reason = PreloadState.FailureReason.HttpError(statusCode = 403), + message = "Forbidden", + ) + ) + ).isEqualTo("preload-state-failed-http-403") + } + + private fun markerFor(reason: PreloadState.FailureReason): String = + PreloadStateMarker.text(PreloadState.Failed(reason = reason, message = "Test failure")) +} diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/E2ETestIds.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/E2ETestIds.swift index 5f3a559ce..26d1bfcab 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/E2ETestIds.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/E2ETestIds.swift @@ -1,5 +1,6 @@ enum E2ETestIds { static let appReady = "checkout-kit-sample-ready" + static let preloadStatePrefix = "preload-state-" enum Cart { static let checkoutReady = "cart-checkout-ready" diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/PreloadStateMarker.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/PreloadStateMarker.swift new file mode 100644 index 000000000..88d2a2ec0 --- /dev/null +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/E2E/PreloadStateMarker.swift @@ -0,0 +1,36 @@ +import ShopifyCheckoutKit + +/// Maps ``PreloadState`` to a preload identifier. +enum PreloadStateMarker { + static func testId(for state: PreloadState) -> String { + "\(E2ETestIds.preloadStatePrefix)\(text(for: state))" + } + + static func text(for state: PreloadState) -> String { + switch state { + case .idle: + return "idle" + case .loading: + return "loading" + case .ready: + return "ready" + case .expired: + return "expired" + case let .failed(reason, _): + return failedText(for: reason) + } + } + + private static func failedText(for reason: PreloadState.FailureReason) -> String { + switch reason { + case let .httpError(statusCode): + return "failed-http-\(statusCode)" + case .navigationFailed: + return "failed-navigation" + case .webContentUnavailable: + return "failed-web-content-unavailable" + case .protocolError: + return "failed-protocol" + } + } +} 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 7d25a39ef..5919f4b21 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift @@ -12,6 +12,7 @@ struct CartView: View { @State var isCompleted: Bool = false @State var showCheckoutSheet: Bool = false @State private var checkoutPreload: CheckoutPreload? + @State private var preloadStateTestId = PreloadStateMarker.testId(for: .idle) @ObservedObject var cartManager: CartManager = .shared @@ -37,6 +38,8 @@ struct CartView: View { } .padding(.bottom, 130) } + .accessibilityElement(children: .contain) + .accessibilityIdentifier(preloadStateTestId) VStack(spacing: DesignSystem.buttonSpacing) { if let cartID = cartManager.cart?.id { @@ -159,6 +162,7 @@ struct CartView: View { ShopifyCheckoutKit.invalidate() checkoutPreload = ShopifyCheckoutKit.preload(checkout: url) checkoutPreload?.onStateChange = { state in + preloadStateTestId = PreloadStateMarker.testId(for: state) print("[Preload] state changed to \(state)") ShopifyCheckoutKit.configuration.logger.log("Preload state changed to \(state)") } diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/PreloadStateMarkerTests.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/PreloadStateMarkerTests.swift new file mode 100644 index 000000000..2301f6005 --- /dev/null +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/PreloadStateMarkerTests.swift @@ -0,0 +1,47 @@ +@testable import CheckoutKitSwiftDemo +import ShopifyCheckoutKit +import XCTest + +class PreloadStateMarkerTests: XCTestCase { + func testLifecycleMarkerTextsMatchTheMaestroFlowAssertions() { + XCTAssertEqual(PreloadStateMarker.text(for: .idle), "idle") + XCTAssertEqual(PreloadStateMarker.text(for: .loading), "loading") + XCTAssertEqual(PreloadStateMarker.text(for: .ready), "ready") + XCTAssertEqual(PreloadStateMarker.text(for: .expired), "expired") + } + + func testHttpFailureMarkerTextIncludesTheStatusCode() { + XCTAssertEqual( + PreloadStateMarker.text(for: .failed(reason: .httpError(statusCode: 403), message: "Forbidden")), + "failed-http-403" + ) + XCTAssertEqual( + PreloadStateMarker.text(for: .failed(reason: .httpError(statusCode: 500), message: "Server error")), + "failed-http-500" + ) + } + + func testNonHttpFailureMarkerTexts() { + XCTAssertEqual( + PreloadStateMarker.text(for: .failed(reason: .navigationFailed, message: "Navigation failed")), + "failed-navigation" + ) + XCTAssertEqual( + PreloadStateMarker.text(for: .failed(reason: .webContentUnavailable, message: "Web content unavailable")), + "failed-web-content-unavailable" + ) + XCTAssertEqual( + PreloadStateMarker.text(for: .failed(reason: .protocolError, message: "Protocol error")), + "failed-protocol" + ) + } + + func testDynamicTestIdsMatchTheMaestroFlowAssertions() { + XCTAssertEqual(PreloadStateMarker.testId(for: .idle), "preload-state-idle") + XCTAssertEqual(PreloadStateMarker.testId(for: .ready), "preload-state-ready") + XCTAssertEqual( + PreloadStateMarker.testId(for: .failed(reason: .httpError(statusCode: 403), message: "Forbidden")), + "preload-state-failed-http-403" + ) + } +}