From 8409a6a634901a715645cfc842440450d9e9feda Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Thu, 30 Jul 2026 18:46:51 +0100 Subject: [PATCH] feat(e2e): add the present-and-close checkout test on all four targets Second test of the rollout. It seeds a cart through the control link, presents checkout, closes it, and asserts the sheet is gone. Swift iOS, Kotlin Android, React Native iOS and React Native Android all run it. Assert on the close control, not on checkout copy: the React Native catalog sells a product called "Email", which the checkout field pattern matches, so copy-based assertions pass on the wrong screen. Extract flows/checkout/close.yaml so the close identifier, which differs by SDK version rather than by platform, has one home. The React Native order flow now calls it instead of carrying its own platform split. The four targets return to different screens after the sheet closes, so this test asserts dismissal only. The tests that place an order assert the cart. Add the checkout tag to the CI matrix and exclude full, because the remaining full tests place a real order and live under tests/react-native/. Pass the installed device to Maestro from both native runners. Maestro otherwise picks a device itself, and it can pick one the app is missing from. Co-Authored-By: Claude Opus 5 (1M context) --- e2e/config/matrix.yml | 5 ++++ e2e/flows/checkout/assert-dismissed.yaml | 14 ++++++++++ .../assert-returned-to-empty-cart.yaml | 16 +---------- e2e/flows/checkout/close.yaml | 27 +++++++++++++++++++ e2e/scripts/run_maestro | 12 ++++++++- ...2e_matrix_to_browserstack_run_plan_test.rb | 4 +-- .../checkout-hardcoded-buyer-identity.yaml | 3 ++- .../shared/checkout-present-and-close.yaml | 16 +++++++++++ platforms/android/scripts/e2e_maestro | 21 +++++++++------ platforms/swift/Scripts/e2e_maestro | 3 ++- 10 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 e2e/flows/checkout/assert-dismissed.yaml create mode 100644 e2e/flows/checkout/close.yaml create mode 100644 e2e/tests/shared/checkout-present-and-close.yaml diff --git a/e2e/config/matrix.yml b/e2e/config/matrix.yml index a551be192..4053c22a4 100644 --- a/e2e/config/matrix.yml +++ b/e2e/config/matrix.yml @@ -7,9 +7,14 @@ tags: include: - launch - cart + - checkout 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-dismissed.yaml b/e2e/flows/checkout/assert-dismissed.yaml new file mode 100644 index 000000000..77ad554a5 --- /dev/null +++ b/e2e/flows/checkout/assert-dismissed.yaml @@ -0,0 +1,14 @@ +appId: ${E2E_APP_ID} +--- +# The close control is the one element that exists only while checkout is on screen, so its +# absence proves dismissal. Asserting on checkout copy instead would be wrong: the React +# Native catalog sells a product called "Email", which the checkout field pattern matches. +# close.yaml lists why the identifier differs per SDK version. Only one of the three is ever +# present, so all three must be absent once the sheet is gone. +- extendedWaitUntil: + notVisible: + id: "shopify_checkout_kit_close_button" + timeout: 30000 +- assertNotVisible: + id: "checkoutKitCloseBtn" +- assertNotVisible: "Close" diff --git a/e2e/flows/checkout/assert-returned-to-empty-cart.yaml b/e2e/flows/checkout/assert-returned-to-empty-cart.yaml index ba7f925ed..34ef280a4 100644 --- a/e2e/flows/checkout/assert-returned-to-empty-cart.yaml +++ b/e2e/flows/checkout/assert-returned-to-empty-cart.yaml @@ -1,20 +1,6 @@ appId: ${E2E_APP_ID} --- -# RN E2E runs against released native SDK artifacts. Android 4.0.0-alpha.2 -# still exposes checkoutKitCloseBtn; #333 normalized the source id to -# shopify_checkout_kit_close_button. When RN nativeSdkVersions point at native -# SDK releases containing #333, replace this platform split with that id. -- runFlow: - when: - platform: android - commands: - - tapOn: - id: checkoutKitCloseBtn -- runFlow: - when: - platform: ios - commands: - - tapOn: "Close" +- runFlow: close.yaml - extendedWaitUntil: visible: diff --git a/e2e/flows/checkout/close.yaml b/e2e/flows/checkout/close.yaml new file mode 100644 index 000000000..8bd61d89f --- /dev/null +++ b/e2e/flows/checkout/close.yaml @@ -0,0 +1,27 @@ +appId: ${E2E_APP_ID} +--- +# The close control carries a different identifier per SDK version, not per platform. +# The Swift and Kotlin samples build the in-repo SDK, which uses the normalized id from +# #333. React Native resolves released 4.0.0-alpha.2 artifacts, where Android still +# exposes checkoutKitCloseBtn and iOS exposes only the "Close" label. Delete the two +# fallback branches once nativeSdkVersions point at releases containing #333. +- runFlow: + when: + visible: + id: "shopify_checkout_kit_close_button" + commands: + - tapOn: + id: "shopify_checkout_kit_close_button" +- runFlow: + when: + visible: + id: "checkoutKitCloseBtn" + commands: + - tapOn: + id: "checkoutKitCloseBtn" +- runFlow: + when: + visible: "Close" + commands: + - tapOn: "Close" +- waitForAnimationToEnd diff --git a/e2e/scripts/run_maestro b/e2e/scripts/run_maestro index 93a138ee0..b3d19513c 100755 --- a/e2e/scripts/run_maestro +++ b/e2e/scripts/run_maestro @@ -6,6 +6,9 @@ # # Every platform runner calls this script, so the environment contract has exactly one # definition. Maestro exits non-zero when a tag or the flows glob matches nothing. +# +# Set E2E_DEVICE_ID when the caller already knows which device holds the installed app. +# Maestro otherwise picks a device itself, and it can pick one the app is missing from. set -euo pipefail PLATFORM="${1:?platform (ios|android) is required}" @@ -13,6 +16,7 @@ APP_ID="${2:?app id is required}" READY_MARKER="${3:?ready marker is required}" INCLUDE_TAGS="${4:-}" EXCLUDE_TAGS="${5:-}" +DEVICE_ID="${E2E_DEVICE_ID:-}" E2E_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -26,7 +30,13 @@ MAESTRO="$("$E2E_ROOT/scripts/maestro_bin")" # than passed. e2e/lib/e2e_matrix_to_browserstack_run_plan.rb derives it the same way for CI. CONTROL_LINK="${APP_ID}://e2e" -MAESTRO_ARGS=(--platform "$PLATFORM" test --config config.yaml) +MAESTRO_ARGS=(--platform "$PLATFORM") + +if [ -n "$DEVICE_ID" ]; then + MAESTRO_ARGS+=(--device "$DEVICE_ID") +fi + +MAESTRO_ARGS+=(test --config config.yaml) if [ -n "$INCLUDE_TAGS" ]; then MAESTRO_ARGS+=(--include-tags "$INCLUDE_TAGS") 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 5b67a9d62..b37a8c13b 100644 --- a/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb +++ b/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb @@ -41,8 +41,8 @@ def test_a_run_executes_the_workspace_root_so_the_config_glob_resolves def test_a_run_carries_the_default_tags run = run_for("swift-ios") - assert_equal ["launch", "cart"], run.fetch("include_tags") - assert_equal ["flaky", "wip"], run.fetch("exclude_tags") + assert_equal ["launch", "cart", "checkout"], run.fetch("include_tags") + assert_equal ["flaky", "wip", "full"], run.fetch("exclude_tags") end def test_an_application_overrides_the_default_tags diff --git a/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml b/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml index 386d21710..47215670e 100644 --- a/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml +++ b/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml @@ -2,7 +2,8 @@ appId: ${E2E_APP_ID} name: React Native checkout - hardcoded buyer identity tags: - checkout - - smoke + # This test submits a real order, so it belongs to the expensive tier. + - full env: # Sample app buyer identity configuration diff --git a/e2e/tests/shared/checkout-present-and-close.yaml b/e2e/tests/shared/checkout-present-and-close.yaml new file mode 100644 index 000000000..9ce835c1e --- /dev/null +++ b/e2e/tests/shared/checkout-present-and-close.yaml @@ -0,0 +1,16 @@ +appId: ${E2E_APP_ID} +name: Present checkout and close it +tags: + - checkout + - smoke + +env: + # No buyerIdentityMode: this test asserts presentation and dismissal, not identity. + E2E_CART_PARAMS: "productIndex=0&quantity=1" +--- +- runFlow: ../../flows/app/bootstrap-cart-from-link.yaml +- runFlow: ../../flows/checkout/present.yaml +- runFlow: ../../flows/checkout/close.yaml +# The four targets return to different screens, so this test asserts dismissal only. +# The tests that place an order assert what the cart holds afterwards. +- runFlow: ../../flows/checkout/assert-dismissed.yaml diff --git a/platforms/android/scripts/e2e_maestro b/platforms/android/scripts/e2e_maestro index 2ffa70a2c..7a27701f9 100755 --- a/platforms/android/scripts/e2e_maestro +++ b/platforms/android/scripts/e2e_maestro @@ -71,17 +71,22 @@ require_maestro() { fi } -require_emulator() { - if ! adb devices | grep -q "[[:space:]]device$"; then - echo "No running Android device or emulator found." >&2 - echo "Start an emulator first, because Maestro drives the device the app runs on." >&2 - return 1 - fi +attached_device() { + adb devices | awk '$2 == "device" { print $1; exit }' } require_maestro -require_emulator + +DEVICE_ID="$(attached_device || true)" +if [ -z "$DEVICE_ID" ]; then + echo "No running Android device or emulator found." >&2 + echo "Start an emulator first, because Maestro drives the device the app runs on." >&2 + exit 1 +fi + +echo "Using device: $DEVICE_ID" "$SAMPLE_DIR/gradlew" -p "$SAMPLE_DIR" :app:installDebug --console=plain -"$REPO_ROOT/e2e/scripts/run_maestro" android "$APP_ID" "$READY_MARKER" "$INCLUDE_TAGS" "$EXCLUDE_TAGS" +E2E_DEVICE_ID="$DEVICE_ID" \ + "$REPO_ROOT/e2e/scripts/run_maestro" android "$APP_ID" "$READY_MARKER" "$INCLUDE_TAGS" "$EXCLUDE_TAGS" diff --git a/platforms/swift/Scripts/e2e_maestro b/platforms/swift/Scripts/e2e_maestro index 0ee23c493..456de3cbb 100755 --- a/platforms/swift/Scripts/e2e_maestro +++ b/platforms/swift/Scripts/e2e_maestro @@ -113,4 +113,5 @@ fi xcrun simctl install "$SIMULATOR_ID" "$APP_PATH" -"$REPO_ROOT/e2e/scripts/run_maestro" ios "$APP_ID" "$READY_MARKER" "$INCLUDE_TAGS" "$EXCLUDE_TAGS" +E2E_DEVICE_ID="$SIMULATOR_ID" \ + "$REPO_ROOT/e2e/scripts/run_maestro" ios "$APP_ID" "$READY_MARKER" "$INCLUDE_TAGS" "$EXCLUDE_TAGS"