From 6c9589fca3993c94ccd0a16687a0592f10c6626d Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Thu, 30 Jul 2026 13:35:22 +0100 Subject: [PATCH] Replace the E2E suite cross product with Maestro tags in CI Every BrowserStack run now executes the whole e2e/tests folder and tags decide what runs inside it. Adding a test file adds no matrix rows: it only needs a tag the matrix already includes. That removes the suites x applications cross product, which grew four rows per new test. matrix.yml gains tests_path and a top-level tags block holding the default include and exclude lists. An application may override either list to adopt a test before the other three carry it. Deliberate deviation from the plan: the plan said add E2E_CONTROL_LINK per application in matrix.yml. The library derives it as "://e2e" instead, because the deep link scheme equals the app id on all four targets. A second copy would drift from app_id. A test pins the derivation for every row. Validation rejects an include tag no test carries. Without it a typo produced a green run that executed nothing. declared_tags reads the flow headers, so both the top-level and per-application lists are checked. Also fixes three real defects found while wiring this up: - RN Android could not receive the control link at all. Its manifest restricted the custom scheme to host "cart" while the contract uses host "e2e". Host "cart" was referenced nowhere else. - The parser tests on all three platforms pinned com.shopify.checkout_kit_android_demo, which no app uses. The Kotlin sample's applicationId is com.shopify.checkoutkit.androiddemo. They now pin the four real schemes plus one unknown scheme as an explicit robustness case. - CI and local runs disagreed on variable names. Both now use E2E_APP_ID, E2E_READY_MARKER and E2E_CONTROL_LINK, and the bootstrap flow composes /cart?... from E2E_CONTROL_LINK itself. The APP_ID and CART_BOOTSTRAP_BASE_LINK bridge variables are gone. The run result payload carries include_tags and exclude_tags so a run that selected nothing is visible in the artifact instead of silently green. Verified: 163 Ruby tests, 29 Swift, 28 Kotlin, 203 jest, dev check all 12 green. Co-Authored-By: Claude Opus 5 (1M context) --- e2e/README.md | 27 ++-- e2e/config/matrix.yml | 12 +- e2e/flows/app/bootstrap-cart-from-link.yaml | 6 +- e2e/flows/checkout/assert-complete.yaml | 2 +- .../assert-returned-to-empty-cart.yaml | 2 +- e2e/flows/checkout/dismiss-active-field.yaml | 2 +- e2e/flows/checkout/fill-contact.yaml | 2 +- e2e/flows/checkout/fill-payment-card.yaml | 2 +- e2e/flows/checkout/fill-shipping-address.yaml | 2 +- e2e/flows/checkout/present.yaml | 2 +- e2e/flows/checkout/submit.yaml | 2 +- .../e2e_matrix_to_browserstack_run_plan.rb | 119 ++++++++++++++---- e2e/scripts/execute_browserstack_run | 11 +- ...2e_matrix_to_browserstack_run_plan_test.rb | 82 ++++++++++++ e2e/tests/react-native/checkout-guest.yaml | 2 +- .../checkout-hardcoded-buyer-identity.yaml | 2 +- .../androiddemo/e2e/E2EControlLinkTest.kt | 16 ++- .../app/src/main/AndroidManifest.template.xml | 3 +- .../src/e2e/__tests__/controlLink.test.ts | 24 ++-- .../react-native/scripts/e2e_maestro_android | 5 +- .../react-native/scripts/e2e_maestro_ios | 5 +- .../E2E/E2EControlLinkTests.swift | 16 ++- 22 files changed, 273 insertions(+), 73 deletions(-) diff --git a/e2e/README.md b/e2e/README.md index 64d9da132..737698039 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -6,8 +6,9 @@ Kit sample apps. Two complementary setups live here: - A **local** React Native suite, run with `dev rn e2e`, that exercises guest and hardcoded buyer identity checkouts from seeded carts through Shopify checkout and back to the app. Tags select which tests run. -- A **CI matrix** that expands applications, OS version tags, and suites into - BrowserStack Maestro run rows, starting with a shared launch smoke. +- A **CI matrix** that expands applications and OS version tags into BrowserStack + Maestro run rows. Every row runs the whole `tests/` folder and tags select what + runs inside it. ## Run locally @@ -90,24 +91,30 @@ the React Native sample configuration, not local in-repo native SDK overrides. ## Matrix -CI runs are described by `config/matrix.yml`. The matrix expands applications, OS -version tags, and suites into a BrowserStack run plan. Because Bitrise has no -built-in matrix support, `e2e/lib/e2e_matrix_to_browserstack_run_plan.rb` -transforms the matrix into a BrowserStack run plan and the pipeline -parallelizes over the resulting rows. +CI runs are described by `config/matrix.yml`. The matrix expands applications and +OS version tags into a BrowserStack run plan. Because Bitrise has no built-in +matrix support, `e2e/lib/e2e_matrix_to_browserstack_run_plan.rb` transforms the +matrix into a BrowserStack run plan and the pipeline parallelizes over the +resulting rows. Current applications: - React Native iOS sample app - React Native Android sample app +- Kotlin Android sample app +- Swift iOS sample app Current OS version tags: - `latest` -Current suites: +Every run executes the whole `tests/` folder. The top-level `tags:` block sets the +default include and exclude lists, and an application may override either one to +adopt a test before the others carry it. Adding a test adds no rows here. -- `tests/shared/launch-smoke.yaml` +The run plan derives `E2E_CONTROL_LINK` as `://e2e`, because the deep link +scheme equals the app id on all four targets. Each run also supplies `E2E_APP_ID` +and `E2E_READY_MARKER`, and the local runners supply the same three names. Validate the matrix: @@ -144,7 +151,7 @@ ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count from a bootstrapped cart with hardcoded buyer identity. - `config/matrix.yml`, `lib/e2e_matrix_to_browserstack_run_plan.rb`, and `scripts/` drive the BrowserStack run plan. -- `tests/shared/launch-smoke.yaml` is the shared launch smoke suite. +- `tests/shared/launch-smoke.yaml` is the shared launch smoke test. ## Shared app contract diff --git a/e2e/config/matrix.yml b/e2e/config/matrix.yml index 2c697ae04..c216f659b 100644 --- a/e2e/config/matrix.yml +++ b/e2e/config/matrix.yml @@ -1,5 +1,14 @@ version: 1 changed_file_filters: .ci/changed-file-filters.yml +tests_path: tests +# Every run executes the whole tests folder and lets tags decide what runs there. +# An application may override either list to adopt a test ahead of the others. +tags: + include: + - launch + exclude: + - flaky + - wip applications: - id: react-native-ios target: react-native @@ -52,6 +61,3 @@ applications: - ciFilters os_version_tags: - latest -suites: - - id: launch-smoke - execute: tests/shared/launch-smoke.yaml diff --git a/e2e/flows/app/bootstrap-cart-from-link.yaml b/e2e/flows/app/bootstrap-cart-from-link.yaml index 73e346d38..439b69925 100644 --- a/e2e/flows/app/bootstrap-cart-from-link.yaml +++ b/e2e/flows/app/bootstrap-cart-from-link.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - launchApp: clearState: true @@ -8,12 +8,12 @@ appId: ${APP_ID} AppleLanguages: "(en)" - extendedWaitUntil: visible: - id: checkout-kit-sample-ready + id: ${E2E_READY_MARKER} timeout: 10000 # Reset app data before testing the deep-link cold-start path. The app is # stopped so openLink is the launch that seeds the cart. - stopApp -- openLink: "${CART_BOOTSTRAP_BASE_LINK}&buyerIdentityMode=${BUYER_IDENTITY_MODE}" +- openLink: "${E2E_CONTROL_LINK}/cart?productIndex=0&quantity=1&buyerIdentityMode=${BUYER_IDENTITY_MODE}" - waitForAnimationToEnd - extendedWaitUntil: visible: diff --git a/e2e/flows/checkout/assert-complete.yaml b/e2e/flows/checkout/assert-complete.yaml index 85015b417..3269468a0 100644 --- a/e2e/flows/checkout/assert-complete.yaml +++ b/e2e/flows/checkout/assert-complete.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - extendedWaitUntil: visible: "${POST_SUBMIT_RESULT_PATTERN}" diff --git a/e2e/flows/checkout/assert-returned-to-empty-cart.yaml b/e2e/flows/checkout/assert-returned-to-empty-cart.yaml index 6ef334dde..ba7f925ed 100644 --- a/e2e/flows/checkout/assert-returned-to-empty-cart.yaml +++ b/e2e/flows/checkout/assert-returned-to-empty-cart.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +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 diff --git a/e2e/flows/checkout/dismiss-active-field.yaml b/e2e/flows/checkout/dismiss-active-field.yaml index 4b672f5ef..9407fd75d 100644 --- a/e2e/flows/checkout/dismiss-active-field.yaml +++ b/e2e/flows/checkout/dismiss-active-field.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - runFlow: when: diff --git a/e2e/flows/checkout/fill-contact.yaml b/e2e/flows/checkout/fill-contact.yaml index 2fdaf65b2..3e0d6dde2 100644 --- a/e2e/flows/checkout/fill-contact.yaml +++ b/e2e/flows/checkout/fill-contact.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - tapOn: text: "^Email( or mobile phone number)?$" diff --git a/e2e/flows/checkout/fill-payment-card.yaml b/e2e/flows/checkout/fill-payment-card.yaml index a9d47ed95..bee1b29f8 100644 --- a/e2e/flows/checkout/fill-payment-card.yaml +++ b/e2e/flows/checkout/fill-payment-card.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - scrollUntilVisible: element: diff --git a/e2e/flows/checkout/fill-shipping-address.yaml b/e2e/flows/checkout/fill-shipping-address.yaml index d06090553..3efa178a8 100644 --- a/e2e/flows/checkout/fill-shipping-address.yaml +++ b/e2e/flows/checkout/fill-shipping-address.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - scrollUntilVisible: element: diff --git a/e2e/flows/checkout/present.yaml b/e2e/flows/checkout/present.yaml index f17a43e15..6ce70df9e 100644 --- a/e2e/flows/checkout/present.yaml +++ b/e2e/flows/checkout/present.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - tapOn: id: checkout-button diff --git a/e2e/flows/checkout/submit.yaml b/e2e/flows/checkout/submit.yaml index bcb82d884..56e90d6b1 100644 --- a/e2e/flows/checkout/submit.yaml +++ b/e2e/flows/checkout/submit.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} --- - scrollUntilVisible: element: diff --git a/e2e/lib/e2e_matrix_to_browserstack_run_plan.rb b/e2e/lib/e2e_matrix_to_browserstack_run_plan.rb index 35c955738..b8b2986f3 100644 --- a/e2e/lib/e2e_matrix_to_browserstack_run_plan.rb +++ b/e2e/lib/e2e_matrix_to_browserstack_run_plan.rb @@ -12,11 +12,14 @@ # parallelizes over it via `run_at`/`count`. # # Today the numbers make the config and the expansion look equivalent - -# 2 applications + 1 os_version_tag + 1 suite, and 2 * 1 * 1 = 2 runs - so it can look +# 4 applications + 1 os_version_tag, and 4 * 1 = 4 runs - so it can look # like a plain YAML-to-JSON copy. The point is the multiplication, not the copy: # adding a single os_version_tag (e.g. a minimum-supported OS) transparently duplicates -# every application x suite across that OS, turning an additive config change into +# every application across that OS, turning an additive config change into # a multiplicative set of runs without hand-writing each one. +# +# Every run executes the whole tests folder. Tags decide what runs inside it, so a new +# test file adds no rows here - it only needs a tag the matrix already includes. class E2EMatrixToBrowserStackRunPlan # Drift between the matrix and the pipeline graph happens in both directions: a stale # branch predating a newly added target, or a build workflow deleted while the matrix @@ -39,10 +42,8 @@ def initialize(config_path, config, changed_files: nil) def expand ensure_valid! selected_applications.flat_map do |application| - os_version_tags.flat_map do |os_version_tag| - suites.map do |suite| - build_run(application, os_version_tag, suite) - end + os_version_tags.map do |os_version_tag| + build_run(application, os_version_tag) end end end @@ -96,11 +97,11 @@ def validation_errors errors << "version must be 1" unless @config.fetch("version", nil) == 1 validate_collection(errors, "applications", applications) validate_collection(errors, "os_version_tags", os_version_tags) - validate_collection(errors, "suites", suites) validate_changed_file_filters(errors) + validate_tests_path(errors) validate_applications(errors) validate_os_version_tags(errors) - validate_suites(errors) + validate_default_tags(errors) errors end @@ -111,27 +112,43 @@ def ensure_valid! private - def build_run(application, os_version_tag, suite) + def build_run(application, os_version_tag) platform = application.fetch("platform") os_version_tag_id = os_version_tag_id(os_version_tag) - suite_id = suite.fetch("id") application_id = application.fetch("id") + app_id = application.fetch("app_id") { - "id" => "#{application_id}-#{os_version_tag_id}-#{suite_id}", + "id" => "#{application_id}-#{os_version_tag_id}", "application_id" => application_id, "target" => application.fetch("target"), "platform" => platform, "os_version_tag" => os_version_tag_id, "device_selector" => device_selector(platform, os_version_tag), - "app_id" => application.fetch("app_id"), + "app_id" => app_id, + "control_link" => control_link(app_id), "artifact_env" => application.fetch("artifact_env"), - "execute" => suite.fetch("execute"), + "execute" => tests_path, + "include_tags" => application_tags(application, "include"), + "exclude_tags" => application_tags(application, "exclude"), "ready_marker" => application.fetch("ready_marker"), - "status_context" => "checkout-kit/e2e/#{application_id}/#{os_version_tag_id}/#{suite_id}" + "status_context" => "checkout-kit/e2e/#{application_id}/#{os_version_tag_id}" } end + # The control link scheme is the app id on all four targets, so deriving it keeps + # one source of truth instead of a second copy that drifts. + def control_link(app_id) + "#{app_id}://e2e" + end + + def application_tags(application, kind) + override = application.fetch("#{kind}_tags", nil) + return Array(override) if override + + Array(default_tags.fetch(kind, nil)) + end + def application_matches_changed_files?(application) filter_names = application_changed_file_filter_names(application) return true if filter_names.empty? @@ -185,8 +202,21 @@ def os_version_tags @config.fetch("os_version_tags", []) || [] end - def suites - @config.fetch("suites", []) || [] + def tests_path + @config.fetch("tests_path", "tests") + end + + def default_tags + @config.fetch("tags", {}) || {} + end + + def declared_tags + @declared_tags ||= Dir.glob("#{tests_path}/**/*.yaml", base: e2e_root).flat_map do |path| + header = File.read(File.join(e2e_root, path)).split("\n---\n").first + YAML.safe_load(header)["tags"] || [] + rescue Psych::Exception + [] + end.uniq end def validate_collection(errors, name, collection) @@ -219,6 +249,7 @@ def validate_applications(errors) errors << "application #{id} missing #{key}" if application.fetch(key, "").to_s.empty? end validate_application_changed_files_filters(errors, application) + validate_application_tags(errors, application) platform = application.fetch("platform", nil) errors << "application #{id} platform must be ios or android" unless ["ios", "android"].include?(platform) end @@ -272,21 +303,57 @@ def safe_os_version_tag_id(os_version_tag) nil end - def validate_suites(errors) - return unless suites.is_a?(Array) + def validate_tests_path(errors) + return if File.directory?(File.join(e2e_root, tests_path)) - validate_unique_ids(errors, "suite", suites) - suites.each do |suite| - id = suite.fetch("id", "") - errors << "suite missing id" if id.to_s.empty? - execute = suite.fetch("execute", "") - errors << "suite #{id} missing execute" if execute.empty? - next if execute.empty? + errors << "tests_path is not a directory: #{tests_path}" + end + + # An include tag no test carries produces a green run that tested nothing. + def validate_default_tags(errors) + ["include", "exclude"].each do |kind| + tags = default_tags.fetch(kind, nil) + next if tags.nil? + + unless tags.is_a?(Array) + errors << "tags #{kind} must be an array" + next + end - errors << "suite #{id} execute path does not exist: #{execute}" unless File.exist?(File.join(e2e_root, execute)) + next unless kind == "include" + + errors.concat(unknown_include_tag_errors(tags) { |tag| "tags include '#{tag}' but no test in tests/ carries it" }) + end + end + + def validate_application_tags(errors, application) + id = application.fetch("id", "") + + ["include", "exclude"].each do |kind| + tags = application.fetch("#{kind}_tags", nil) + next if tags.nil? + + unless tags.is_a?(Array) + errors << "application #{id} #{kind}_tags must be an array" + next + end + + next unless kind == "include" + + errors.concat( + unknown_include_tag_errors(tags) do |tag| + "application #{id} include_tags '#{tag}' but no test in tests/ carries it" + end + ) end end + def unknown_include_tag_errors(tags) + return [] if declared_tags.empty? + + (tags - declared_tags).map { |tag| yield(tag) } + end + def validate_unique_ids(errors, label, collection) ids = collection.map { |item| item.fetch("id", nil) } errors << "#{label} ids must be unique" unless ids.compact.uniq.length == ids.compact.length diff --git a/e2e/scripts/execute_browserstack_run b/e2e/scripts/execute_browserstack_run index 5b313beaa..59c488422 100755 --- a/e2e/scripts/execute_browserstack_run +++ b/e2e/scripts/execute_browserstack_run @@ -147,9 +147,14 @@ class BrowserStackRunExecutor customBuildName: run.fetch("id"), devices: [device], execute: [run.fetch("execute")], + tags: { + includeTags: run.fetch("include_tags"), + excludeTags: run.fetch("exclude_tags") + }, setEnvVariables: { E2E_APP_ID: run.fetch("app_id"), - E2E_READY_MARKER: run.fetch("ready_marker") + E2E_READY_MARKER: run.fetch("ready_marker"), + E2E_CONTROL_LINK: run.fetch("control_link") } } response = @client.start_build(run.fetch("platform"), body) @@ -205,6 +210,8 @@ class BrowserStackRunExecutor "target" => run.fetch("target"), "os_version_tag" => run.fetch("os_version_tag"), "execute" => run.fetch("execute"), + "include_tags" => run.fetch("include_tags"), + "exclude_tags" => run.fetch("exclude_tags"), "device_selector" => device.fetch("device_selector"), "resolved_device" => device.fetch("resolved_device"), "resolved_os_version" => device.fetch("resolved_os_version"), @@ -228,6 +235,8 @@ class BrowserStackRunExecutor "target" => run["target"], "os_version_tag" => run["os_version_tag"], "execute" => run["execute"], + "include_tags" => run["include_tags"], + "exclude_tags" => run["exclude_tags"], "device_selector" => run["device_selector"], "resolved_device" => device["resolved_device"] || run["device_selector"], "resolved_os_version" => device["resolved_os_version"], 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 c9745642e..ca79f893e 100644 --- a/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb +++ b/e2e/test/e2e_matrix_to_browserstack_run_plan_test.rb @@ -18,6 +18,88 @@ def selected_ids(changed_files) plan(changed_files: changed_files).selected_applications.map { |application| application.fetch("id") } end + def run_for(application_id, changed_files: nil) + plan(changed_files: changed_files).expand.find { |run| run.fetch("application_id") == application_id } + end + + def test_expand_produces_one_run_per_application_and_os_version_tag + runs = plan.expand + + assert_equal 4, runs.length + assert_equal( + ["react-native-ios-latest", "react-native-android-latest", "kotlin-android-latest", "swift-ios-latest"], + runs.map { |run| run.fetch("id") } + ) + end + + def test_a_run_executes_the_whole_tests_folder + assert_equal "tests", run_for("swift-ios").fetch("execute") + end + + def test_a_run_carries_the_default_tags + run = run_for("swift-ios") + + assert_equal ["launch"], run.fetch("include_tags") + assert_equal ["flaky", "wip"], run.fetch("exclude_tags") + end + + def test_an_application_overrides_the_default_tags + config = base_config + config.fetch("applications").first["include_tags"] = ["launch", "checkout"] + config.fetch("applications").first["exclude_tags"] = ["wip"] + + run = plan(config: config).expand.first + + assert_equal ["launch", "checkout"], run.fetch("include_tags") + assert_equal ["wip"], run.fetch("exclude_tags") + end + + def test_a_control_link_follows_the_app_id_on_every_application + plan.expand.each do |run| + assert_equal "#{run.fetch("app_id")}://e2e", run.fetch("control_link") + end + end + + def test_a_status_context_no_longer_names_a_suite + assert_equal "checkout-kit/e2e/swift-ios/latest", run_for("swift-ios").fetch("status_context") + end + + def test_validation_errors_flags_an_include_tag_no_test_carries + config = base_config + config.fetch("tags")["include"] = ["launch", "teleport"] + + errors = plan(config: config).validation_errors + + assert_includes errors, "tags include 'teleport' but no test in tests/ carries it" + end + + def test_validation_errors_flags_an_application_include_tag_no_test_carries + config = base_config + config.fetch("applications").first["include_tags"] = ["teleport"] + + errors = plan(config: config).validation_errors + + assert_includes errors, "application react-native-ios include_tags 'teleport' but no test in tests/ carries it" + end + + def test_validation_errors_flags_non_array_include_tags + config = base_config + config.fetch("applications").first["include_tags"] = "launch" + + errors = plan(config: config).validation_errors + + assert_includes errors, "application react-native-ios include_tags must be an array" + end + + def test_validation_errors_flags_a_missing_tests_path + config = base_config + config["tests_path"] = "does-not-exist" + + errors = plan(config: config).validation_errors + + assert_includes errors, "tests_path is not a directory: does-not-exist" + end + def test_nil_changed_files_selects_all_applications assert_equal ["react-native-ios", "react-native-android", "kotlin-android", "swift-ios"], selected_ids(nil) end diff --git a/e2e/tests/react-native/checkout-guest.yaml b/e2e/tests/react-native/checkout-guest.yaml index 639540bee..1d76f63cf 100644 --- a/e2e/tests/react-native/checkout-guest.yaml +++ b/e2e/tests/react-native/checkout-guest.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} name: React Native checkout - guest tags: - checkout diff --git a/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml b/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml index c126dc48d..f02b721c5 100644 --- a/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml +++ b/e2e/tests/react-native/checkout-hardcoded-buyer-identity.yaml @@ -1,4 +1,4 @@ -appId: ${APP_ID} +appId: ${E2E_APP_ID} name: React Native checkout - hardcoded buyer identity tags: - checkout diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2EControlLinkTest.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2EControlLinkTest.kt index 9aedf2f1f..5e8fbd011 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2EControlLinkTest.kt +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2EControlLinkTest.kt @@ -15,9 +15,21 @@ class E2EControlLinkTest { @Test fun `parses every app scheme`() { val expected = E2EControlLink.Cart(productIndex = 0) + val schemes = listOf( + "com.shopify.checkoutkit.androiddemo", + "com.shopify.checkoutkit.swiftdemo", + "com.shopify.checkoutkit.reactnativedemo", + ) - assertThat(E2EControlLink.parse("com.shopify.checkoutkit.androiddemo://e2e/cart?productIndex=0")).isEqualTo(expected) - assertThat(E2EControlLink.parse("com.shopify.checkoutkit.swiftdemo://e2e/cart?productIndex=0")).isEqualTo(expected) + schemes.forEach { scheme -> + assertThat(E2EControlLink.parse("$scheme://e2e/cart?productIndex=0")).isEqualTo(expected) + } + } + + @Test + fun `parses a scheme the matrix does not declare`() { + assertThat(E2EControlLink.parse("com.example.anything://e2e/cart?productIndex=0")) + .isEqualTo(E2EControlLink.Cart(productIndex = 0)) } @Test diff --git a/platforms/react-native/sample/android/app/src/main/AndroidManifest.template.xml b/platforms/react-native/sample/android/app/src/main/AndroidManifest.template.xml index e0d538711..66d1050ce 100644 --- a/platforms/react-native/sample/android/app/src/main/AndroidManifest.template.xml +++ b/platforms/react-native/sample/android/app/src/main/AndroidManifest.template.xml @@ -41,7 +41,8 @@ - + + diff --git a/platforms/react-native/sample/src/e2e/__tests__/controlLink.test.ts b/platforms/react-native/sample/src/e2e/__tests__/controlLink.test.ts index 41f2c772d..e91e1579e 100644 --- a/platforms/react-native/sample/src/e2e/__tests__/controlLink.test.ts +++ b/platforms/react-native/sample/src/e2e/__tests__/controlLink.test.ts @@ -20,17 +20,23 @@ describe('parseControlLink', () => { it('parses every app scheme', () => { const expected = {command: 'cart', productIndex: 0, quantity: 1}; + const schemes = [ + 'com.shopify.checkoutkit.reactnativedemo', + 'com.shopify.checkoutkit.swiftdemo', + 'com.shopify.checkoutkit.androiddemo', + ]; + + schemes.forEach((scheme) => { + expect(parseControlLink(`${scheme}://e2e/cart?productIndex=0`)).toEqual( + expected, + ); + }); + }); + it('parses a scheme the matrix does not declare', () => { expect( - parseControlLink( - 'com.shopify.checkoutkit.reactnativedemo://e2e/cart?productIndex=0', - ), - ).toEqual(expected); - expect( - parseControlLink( - 'com.shopify.checkoutkit.androiddemo://e2e/cart?productIndex=0', - ), - ).toEqual(expected); + parseControlLink('com.example.anything://e2e/cart?productIndex=0'), + ).toEqual({command: 'cart', productIndex: 0, quantity: 1}); }); it('parses the reset command', () => { diff --git a/platforms/react-native/scripts/e2e_maestro_android b/platforms/react-native/scripts/e2e_maestro_android index 6daa85df4..69852b9e7 100755 --- a/platforms/react-native/scripts/e2e_maestro_android +++ b/platforms/react-native/scripts/e2e_maestro_android @@ -9,7 +9,7 @@ METRO_LOG="${TMPDIR:-/tmp}/checkout-kit-rn-android-metro.log" METRO_PID="" E2E_ENV_FILE="" APP_ID="com.shopify.checkoutkit.reactnativedemo" -CART_BOOTSTRAP_BASE_LINK="${APP_ID}://e2e/cart?productIndex=0&quantity=1" +CONTROL_LINK="${APP_ID}://e2e" READY_MARKER="checkout-kit-sample-ready" INCLUDE_TAGS="" EXCLUDE_TAGS="flaky,wip" @@ -135,9 +135,8 @@ ENVFILE="$E2E_ENV_FILE" pnpm sample android --extra-params "--refresh-dependenci fi "$MAESTRO" "${MAESTRO_ARGS[@]}" \ - -e "APP_ID=${APP_ID}" \ -e "E2E_APP_ID=${APP_ID}" \ -e "E2E_READY_MARKER=${READY_MARKER}" \ - -e "CART_BOOTSTRAP_BASE_LINK=${CART_BOOTSTRAP_BASE_LINK}" \ + -e "E2E_CONTROL_LINK=${CONTROL_LINK}" \ tests ) diff --git a/platforms/react-native/scripts/e2e_maestro_ios b/platforms/react-native/scripts/e2e_maestro_ios index 2016c0b44..076b21b13 100755 --- a/platforms/react-native/scripts/e2e_maestro_ios +++ b/platforms/react-native/scripts/e2e_maestro_ios @@ -9,7 +9,7 @@ METRO_LOG="${TMPDIR:-/tmp}/checkout-kit-rn-ios-metro.log" METRO_PID="" E2E_ENV_FILE="" APP_ID="com.shopify.checkoutkit.reactnativedemo" -CART_BOOTSTRAP_BASE_LINK="${APP_ID}://e2e/cart?productIndex=0&quantity=1" +CONTROL_LINK="${APP_ID}://e2e" READY_MARKER="checkout-kit-sample-ready" INCLUDE_TAGS="" EXCLUDE_TAGS="flaky,wip" @@ -136,9 +136,8 @@ ENVFILE="$E2E_ENV_FILE" pnpm sample ios fi "$MAESTRO" "${MAESTRO_ARGS[@]}" \ - -e "APP_ID=${APP_ID}" \ -e "E2E_APP_ID=${APP_ID}" \ -e "E2E_READY_MARKER=${READY_MARKER}" \ - -e "CART_BOOTSTRAP_BASE_LINK=${CART_BOOTSTRAP_BASE_LINK}" \ + -e "E2E_CONTROL_LINK=${CONTROL_LINK}" \ tests ) diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2EControlLinkTests.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2EControlLinkTests.swift index 0eddeb9af..3d0c2ba31 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2EControlLinkTests.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2EControlLinkTests.swift @@ -10,9 +10,21 @@ class E2EControlLinkTests: XCTestCase { func testParsesEveryAppScheme() throws { let expected = E2EControlLink.cart(.init(productIndex: 0, quantity: 1)) + let schemes = [ + "com.shopify.checkoutkit.swiftdemo", + "com.shopify.checkoutkit.androiddemo", + "com.shopify.checkoutkit.reactnativedemo" + ] + + for scheme in schemes { + XCTAssertEqual(try E2EControlLink.parse("\(scheme)://e2e/cart?productIndex=0"), expected) + } + } + + func testParsesASchemeTheMatrixDoesNotDeclare() throws { + let expected = E2EControlLink.cart(.init(productIndex: 0, quantity: 1)) - XCTAssertEqual(try E2EControlLink.parse("com.shopify.checkoutkit.swiftdemo://e2e/cart?productIndex=0"), expected) - XCTAssertEqual(try E2EControlLink.parse("com.shopify.checkoutkit.androiddemo://e2e/cart?productIndex=0"), expected) + XCTAssertEqual(try E2EControlLink.parse("com.example.anything://e2e/cart?productIndex=0"), expected) } func testParsesTheResetCommand() throws {