Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,27 @@ CUSTOMER_ACCOUNT_API_CLIENT_ID=
CUSTOMER_ACCOUNT_API_SHOP_ID=
CUSTOMER_ACCOUNT_API_VERSION=2026-04

# Buyer identity defaults used by sample apps
# Customer account used by the Maestro account journey (optional).
# CI supplies these from Bitrise secrets. Leave both blank to skip the account tests.
E2E_CUSTOMER_ACCOUNT_EMAIL=
E2E_CUSTOMER_ACCOUNT_CODE=

# User agent suffix the samples add to the customer account login web view (optional).
# CI supplies this from Bitrise secrets. Leave it blank for normal sample use.
CUSTOM_USER_AGENT=

# Buyer identity defaults used by sample apps.
# Keep the address in the E2E shop's own country. A foreign address changes the billing
# country, the province list, the postal field label and the currency, so the checkout
# flows then look for fields that the form does not show.
EMAIL=checkout-kit@example.com
ADDRESS_1=650 King Street
ADDRESS_1=700 S Flower St
ADDRESS_2=Shopify HQ
CITY=Toronto
CITY=Los Angeles
COMPANY=Shopify
COUNTRY=CA
COUNTRY=US
FIRST_NAME=Evelyn
LAST_NAME=Hartley
PROVINCE=ON
ZIP=M5V 1M7
PHONE=+14165550100
PROVINCE=CA
ZIP=90017
PHONE=+12135550142
15 changes: 15 additions & 0 deletions e2e/BITRISE.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,24 @@ The launch smoke suite sends only non-sensitive Maestro environment values to Br

- `E2E_APP_ID`
- `E2E_READY_MARKER`
- `E2E_CONTROL_LINK`

Do not pass storefront tokens or customer data through BrowserStack Maestro environment variables without explicit review, because those values are visible in BrowserStack dashboards.

## Account journey secrets

The account journey signs a test customer in, so it needs two more Maestro values. Both are reviewed exceptions to the rule above, because the BrowserStack dashboard is private to this organization and the values never enter the repository.

| Variable | Where it is used | Purpose |
| ---------------------------- | -------------------------------------- | ---------------------------------------------------------------- |
| `E2E_CUSTOMER_ACCOUNT_EMAIL` | Maestro environment | Address the sign-in flow types on the hosted login page. |
| `E2E_CUSTOMER_ACCOUNT_CODE` | Maestro environment | Verification code the sign-in flow types. |
| `CUSTOM_USER_AGENT` | Sample app build, through `.env` | Marks the login web view as a test client. The code fails without it. |

Add all three as secret Bitrise environment variables. Leave them out of any log, any pull request, and any file in this repository. `scripts/setup_storefront_env` reads `CUSTOM_USER_AGENT` from the root `.env` and writes it into each generated sample configuration, all of which are ignored by git.

A run with these values missing skips the account tests instead of failing, so a fork without the secrets still gets a green suite.

## GitHub reporting

The `e2e-report` workflow creates commit statuses, Check Runs, and sticky PR comments using the short-lived token generated by the Bitrise GitHub App.
Expand Down
2 changes: 2 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ ruby e2e/scripts/e2e_matrix_to_browserstack_run_plan count
already carries the contact and the delivery address.
- `tests/shared/checkout-guest.yaml` orders from an empty identity, so checkout
asks for the contact and the delivery address as well as the payment.
- `tests/shared/checkout-customer-account.yaml` signs a customer in, then orders.
Checkout reads the contact and the delivery address from the account.
- `scripts/run_local_e2e` builds and installs any of the four local targets.
- `scripts/run_maestro` is their single Maestro invocation. It holds the
environment contract and the workspace root rule in one place.
Expand Down
2 changes: 2 additions & 0 deletions e2e/config/matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ tags:
include:
- launch
- checkout
# execute_browserstack_run drops this tag again when the account secrets are absent.
- account
exclude:
- flaky
- wip
Expand Down
63 changes: 4 additions & 59 deletions e2e/flows/app/bootstrap-cart-from-link.yaml
Original file line number Diff line number Diff line change
@@ -1,61 +1,6 @@
appId: ${E2E_APP_ID}
---
- launchApp:
clearState: true
arguments:
# iOS-only launch arguments; Android ignores them.
AppleLocale: en_US
AppleLanguages: "(en)"
# Avoid relaunching healthy apps. BrowserStack can report the state-clearing reinstall and
# launch as complete while iOS remains on SpringBoard, so probe briefly before recovering.
- extendedWaitUntil:
visible:
id: ${E2E_READY_MARKER}
timeout: 10000
optional: true
- runFlow:
when:
notVisible:
id: ${E2E_READY_MARKER}
commands:
- retry:
maxRetries: 1
commands:
- launchApp:
arguments:
AppleLocale: en_US
AppleLanguages: "(en)"
- extendedWaitUntil:
visible:
id: ${E2E_READY_MARKER}
timeout: 60000
# The wait above can match a view hierarchy the dying process left behind, which sends the
# link seconds before the new activity exists. Android then drops it, because a deep link
# has no delivery guarantee. One repeat covers that, and the command is safe to repeat:
# every cart command resets the cart before it seeds one.
- retry:
maxRetries: 1
commands:
# The link goes to the running app. launchApp cleared the state above, so the cart is
# already empty, and a running app is what the account journey needs later: iOS delivers
# a cold-start URL through the scene connect options and Android through the launch
# intent, so a stopped app would need a second entry point on every platform.
# Each test supplies E2E_CART_PARAMS, so tests that do not care about buyer
# identity leave the app's configured mode alone.
- openLink: "${E2E_CONTROL_LINK}/cart?${E2E_CART_PARAMS}"
- waitForAnimationToEnd
# iOS asks the user to confirm before it hands a custom scheme to an app. Android
# does not, so the tap is conditional rather than platform-gated.
- runFlow:
when:
visible: "Open"
commands:
- tapOn: "Open"
- waitForAnimationToEnd
# The cart marker only appears after the controller resolved a variant, created a
# cart, added the line, and navigated. Reaching it proves the whole command path.
# That is two network round trips, so the timeout covers a slow device on a slow shop.
- extendedWaitUntil:
visible:
id: cart-checkout-ready
timeout: 60000
# Most tests want a clean app holding one seeded cart. The account journey needs the two
# halves apart, because it signs in between them, so each half is its own flow.
- runFlow: launch.yaml
- runFlow: seed-cart-from-link.yaml
10 changes: 10 additions & 0 deletions e2e/flows/app/confirm-open-link.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
appId: ${E2E_APP_ID}
---
# iOS asks the user to confirm before it hands a custom scheme to an app. Android does
# not, so the tap is conditional rather than platform-gated.
- runFlow:
when:
visible: "Open"
commands:
- tapOn: "Open"
- waitForAnimationToEnd
31 changes: 31 additions & 0 deletions e2e/flows/app/launch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
appId: ${E2E_APP_ID}
---
- launchApp:
clearState: true
arguments:
# iOS-only launch arguments; Android ignores them.
AppleLocale: en_US
AppleLanguages: "(en)"
# Avoid relaunching healthy apps. BrowserStack can report the state-clearing reinstall and
# launch as complete while iOS remains on SpringBoard, so probe briefly before recovering.
- extendedWaitUntil:
visible:
id: ${E2E_READY_MARKER}
timeout: 10000
optional: true
- runFlow:
when:
notVisible:
id: ${E2E_READY_MARKER}
commands:
- retry:
maxRetries: 1
commands:
- launchApp:
arguments:
AppleLocale: en_US
AppleLanguages: "(en)"
- extendedWaitUntil:
visible:
id: ${E2E_READY_MARKER}
timeout: 60000
24 changes: 24 additions & 0 deletions e2e/flows/app/seed-cart-from-link.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
appId: ${E2E_APP_ID}
---
# The launch wait can match a view hierarchy the dying process left behind, which sends
# the link seconds before the new activity exists. Android then drops it, because a deep
# link has no delivery guarantee. One repeat covers that, and the command is safe to
# repeat: every cart command resets the cart before it seeds one.
- retry:
maxRetries: 1
commands:
# The link goes to the running app. A running app is what the account journey needs:
# iOS delivers a cold-start URL through the scene connect options and Android through
# the launch intent, so a stopped app would need a second entry point on every
# platform. Each test supplies E2E_CART_PARAMS, so tests that do not care about buyer
# identity leave the app's configured mode alone.
- openLink: "${E2E_CONTROL_LINK}/cart?${E2E_CART_PARAMS}"
- waitForAnimationToEnd
- runFlow: confirm-open-link.yaml
# The cart marker only appears after the controller resolved a variant, created a
# cart, added the line, and navigated. Reaching it proves the whole command path.
# That is two network round trips, so the timeout covers a slow device on a slow shop.
- extendedWaitUntil:
visible:
id: cart-checkout-ready
timeout: 60000
52 changes: 52 additions & 0 deletions e2e/flows/app/sign-in-customer-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
appId: ${E2E_APP_ID}
---
# The control link opens the sample's login screen, which hosts the Shopify sign-in page
# in a WebView. Every target configures that WebView without persistence, so no session
# survives a launch and every run types the address again.
- openLink: "${E2E_CONTROL_LINK}/signIn"
- waitForAnimationToEnd
- runFlow: confirm-open-link.yaml
- extendedWaitUntil:
visible:
text: "^Email( address)?$"
timeout: 60000
- tapOn:
text: "^Email( address)?$"
- inputText: ${E2E_CUSTOMER_ACCOUNT_EMAIL}
# The page reads the address as it is typed. An address that owns a Shop account gets a
# "Continue with shop" button, and every other address gets "Continue". Only the code route
# works on all four targets, so this flow takes "Use email instead" whenever the page shows
# it, and falls back to "Continue".
- extendedWaitUntil:
visible:
text: "^(Continue|Use email instead)$"
timeout: 30000
# The "Continue" branch comes first and reads the same element the second branch taps, so
# only one of the two ever runs. The other order lets a "Continue" on the code page match
# after "Use email instead" has already moved the page on.
- runFlow:
when:
notVisible: "^Use email instead$"
visible: "^Continue$"
commands:
- tapOn:
text: "^Continue$"
- runFlow:
when:
visible: "^Use email instead$"
commands:
- tapOn:
text: "^Use email instead$"
- extendedWaitUntil:
visible:
text: "^6-digit code$"
timeout: 30000
- tapOn:
text: "^6-digit code$"
- inputText: ${E2E_CUSTOMER_ACCOUNT_CODE}
# The page submits the code itself, and each sample then shows its account screen. Swift
# dismisses the login sheet, Android leaves the login route, and React Native goes back.
- extendedWaitUntil:
visible:
id: account-signed-in-view
timeout: 60000
12 changes: 12 additions & 0 deletions e2e/flows/checkout/assert-buyer-prefilled.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
appId: ${E2E_APP_ID}
---
# The signed-in buyer is the point of this journey. Checkout reads the contact address and
# the saved delivery address from the account, so both appear before the test types
# anything. A guest cart shows an empty contact field and a delivery form instead, so these
# two assertions separate a real customer cart from a cart that lost its buyer identity.
- extendedWaitUntil:
visible:
text: "${E2E_CUSTOMER_ACCOUNT_EMAIL}"
timeout: 30000
- assertVisible:
text: "^Ship to$"
4 changes: 2 additions & 2 deletions e2e/flows/checkout/fill-billing-address.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ appId: ${E2E_APP_ID}
# Country/Region and Province already hold the shop defaults, so this flow leaves
# them alone and supplies an address that suits those defaults.
- tapOn:
text: "^First name$"
text: "^First name( \\(optional\\))?$"
- inputText: "${BILLING_FIRST_NAME}"
- runFlow: dismiss-active-field.yaml
- tapOn:
Expand All @@ -32,7 +32,7 @@ appId: ${E2E_APP_ID}
- inputText: "${BILLING_CITY}"
- runFlow: dismiss-active-field.yaml
- tapOn:
text: "^Postal code$"
text: "^${BILLING_POSTAL_FIELD_LABEL}$"
- inputText: "${BILLING_POSTAL_CODE}"
- runFlow: dismiss-active-field.yaml
- waitForAnimationToEnd
27 changes: 4 additions & 23 deletions e2e/flows/checkout/fill-contact.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
appId: ${E2E_APP_ID}
---
- tapOn:
text: "^Email( or mobile phone number)?$"
- waitForAnimationToEnd
- inputText: "${EMAIL}"
- runFlow: dismiss-active-field.yaml
- extendedWaitUntil:
visible: "^${EMAIL}$"
- tapOn:
text: "^First name( \\(optional\\))?$"
- waitForAnimationToEnd
- inputText: "${FIRST_NAME}"
- runFlow: dismiss-active-field.yaml
- scrollUntilVisible:
element:
text: "^Last name$"
direction: DOWN
visibilityPercentage: 100
centerElement: true
- tapOn:
text: "^Last name$"
- waitForAnimationToEnd
- inputText: "${LAST_NAME}"
- runFlow: dismiss-active-field.yaml
# A guest types both halves. A signed-in buyer arrives with the email already attached,
# so the account journey runs fill-name.yaml on its own.
- runFlow: fill-email.yaml
- runFlow: fill-name.yaml
9 changes: 9 additions & 0 deletions e2e/flows/checkout/fill-email.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
appId: ${E2E_APP_ID}
---
- tapOn:
text: "^Email( or mobile phone number)?$"
- waitForAnimationToEnd
- inputText: "${EMAIL}"
- runFlow: dismiss-active-field.yaml
- extendedWaitUntil:
visible: "^${EMAIL}$"
22 changes: 22 additions & 0 deletions e2e/flows/checkout/fill-name.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
appId: ${E2E_APP_ID}
---
# A signed-in buyer can arrive with either name already filled, and `inputText` appends
# rather than replaces, so each field is cleared before it is typed.
- tapOn:
text: "^First name( \\(optional\\))?$"
- waitForAnimationToEnd
- eraseText
- inputText: "${FIRST_NAME}"
- runFlow: dismiss-active-field.yaml
- scrollUntilVisible:
element:
text: "^Last name$"
direction: DOWN
visibilityPercentage: 100
centerElement: true
- tapOn:
text: "^Last name$"
- waitForAnimationToEnd
- eraseText
- inputText: "${LAST_NAME}"
- runFlow: dismiss-active-field.yaml
Loading
Loading