Maestro Deck
Guides

iOS vs Android testing tips

Cross-platform Maestro testing — what's the same, what's different, and how to keep one flow working on both iOS and Android.

Maestro's selling point is that the same YAML drives both iOS and Android. In practice, "same YAML" is true for ~80% of selectors and ~60% of gestures. The remaining edge cases are where teams burn hours. This guide is the cheat sheet.

TL;DR

Prefer accessibility IDs over text selectors. Use runFlow with platform-specific subflows for genuinely divergent screens. Don't fight the platform — if iOS uses a tab bar and Android uses a navigation drawer, write two short flows, not one branching one.

What's identical

  • appId, tags, env config block.
  • launchApp, tapOn, inputText, assertVisible, assertNotVisible.
  • Selector by id (matches accessibility identifier on iOS, view-id-resource-name on Android).
  • Selector by text.
  • Scrolling: scroll, swipe, scrollUntilVisible.

For a well-instrumented app, 80% of flows look identical on both platforms.

What differs

System dialogs

  • iOS: permission prompts ("Allow notifications?", "Allow location?") render in a system overlay outside the app's hierarchy. Use tapOn: "Allow" — Maestro looks at the foreground window.
  • Android: permission dialogs are also system-level but the button text varies by API level ("Allow", "While using the app", "Only this time").

A safe pattern:

- runFlow:
    when:
      visible: "Allow"
    file: subflows/grant-permission.yaml

Keyboard

  • iOS: the return key has no consistent text label. Use pressKey: Enter.
  • Android: soft keyboard varies by manufacturer's keyboard app on physical devices. Stick to the AOSP keyboard (Gboard) on emulators.

Back button

  • iOS: no system back button. Use tapOn on the screen's chevron or swipe: { direction: RIGHT, start: "0%, 50%" }.
  • Android: pressKey: Back works system-wide.

A platform-conditional helper:

# subflows/go-back.yaml
- runFlow:
    when:
      platform: ANDROID
    file: subflows/back-android.yaml
- runFlow:
    when:
      platform: IOS
    file: subflows/back-ios.yaml

Web views

Both platforms expose the WebView hierarchy, but iOS's WKWebView reports element bounds in points; Android's WebView in pixels. tapOn by selector is fine on both. Coordinate-based taps are not portable.

App lifecycle

  • iOS: launchApp cold-starts every time by default.
  • Android: launchApp may resume an existing process. Use clearState: true to force a cold start:
- launchApp:
    clearState: true

Authoring portable selectors

Rule of thumb, in order of preference:

  1. Accessibility ID (id:) — most robust, survives translations.
  2. Text (text:) — fragile to copy changes and i18n.
  3. Index within a parent — almost never use; a layout change breaks it silently.

Ask your iOS and Android engineers to set the same accessibilityIdentifier on iOS and android:contentDescription (or testTag in Compose) on Android. Even better: a shared config file that both platforms generate from.

// iOS
button.accessibilityIdentifier = "auth.signin.button"
// Android Compose
Button(modifier = Modifier.testTag("auth.signin.button")) { ... }

Now the same flow works on both.

When to fork the flow

If a feature has genuinely different UX per platform — say, a tab bar on iOS and a nav drawer on Android — don't write a flow with five conditionals. Write checkout-ios.yaml and checkout-android.yaml, tag them appropriately, and run only the relevant one:

maestro test --device "iPhone 15" --include-tags=ios-only,common .maestro/
maestro test --device "Pixel 7" --include-tags=android-only,common .maestro/

This is cleaner than maintaining a megaflow with platform branches.

Locale and time zone

Set both explicitly in your runner. Default device locale leaks into screenshots and breaks text: selectors when an engineer adds a new language to the bundle:

  • iOS: xcrun simctl spawn booted defaults write -g AppleLanguages "(en)"
  • Android: adb shell am broadcast -a android.intent.action.LOCALE_CHANGED

Common pitfalls

  • Different OS versions. A flow that works on iOS 17 may fail on iOS 16 because a system control was renamed. Pin OS in CI.
  • Compose vs XML on Android. Compose UIs may not expose the same view IDs as XML. Use testTag consistently.
  • Notch and dynamic island. Coordinate-based taps near the top of the screen drift across device sizes. Avoid them.

On this page