Skip to main content

Mobile Test YAML

Mobile tests use the same YAML shape as UI tests, with mobile-specific actions and selectors. Same steps list, same {{variable}} substitution rules, same assertion patterns.

Minimal example

steps:
- action: tap
selector: accessibility:test-Username
- action: type
selector: accessibility:test-Username
value: standard_user
- action: tap
selector: accessibility:test-Password
- action: type
selector: accessibility:test-Password
value: secret_sauce
- action: tap
selector: accessibility:test-LOGIN
- action: assert_visible
selector: accessibility:test-PRODUCTS

Selectors

Mobile uses Appium's selector syntax — same as Appium docs.

PrefixMeansExample
accessibility:Accessibility ID (preferred)accessibility:test-Username
id:Native resource id (Android) / accessibility id (iOS)id:com.example:id/username
xpath:XPathxpath://*[@text='Login']
class:Class nameclass:android.widget.Button
predicate:iOS NSPredicate (iOS only)predicate:label == 'Login'
classchain:iOS class chain (iOS only)classchain:**/XCUIElementTypeButton[1]
text:Visible text, matched exactlytext:Don't allow

Always prefer accessibility IDs — they're stable across UI redesigns and work the same on Android + iOS when developers tag elements correctly.

Matching on text

text: matches the whole label, not part of it — text:Allow will not match a Don't allow button. Write the label as you'd type it; quote style and spacing are handled for you:

  • Curly quotes and straight ones are treated as the same character. Android renders Don't allow with a curly apostrophe, but text:Don't allow matches it. The two look identical on screen and in a bug report, so you should never have to care which one the app used.
  • Non-breaking spaces match ordinary spaces.
  • Leading and trailing spaces are ignored, and runs of spaces count as one.

Android and iOS

accessibility: means the same thing on both platforms — it maps to content-desc on Android and to the accessibility identifier on iOS. It's the selector to reach for if you want one test to run on both.

text: matches what's on screen, and the two platforms store that differently. Android keeps it in text; iOS keeps it in label, or in value for a field showing its contents. The platform is read from the running session, so you write text:Sign In either way and we match the right attribute.

On iOS, text: deliberately does not match the accessibility identifier. In BrowserStack's own sample app there's a button whose identifier is Text Button and whose visible label is Text — so text:Text finds it and text:Text Button does not. That's on purpose: matching identifiers too would let a selector quietly grab a control whose label reads something else entirely.

predicate: and classchain: are iOS-only. Used in an Android test they raise an error naming the problem, rather than silently matching nothing.

Text is the most fragile thing to match on — it changes with copy edits and breaks in every other language. Use it when there's nothing better, and prefer accessibility:.

Actions

Action names are case-insensitive (tap == TAP). Several have aliases — both spellings execute identically.

An unknown action FAILS the step

It used to be reported SKIPPED, so a typo produced a green-looking run that had tested nothing. Since release-repo #604 the step fails and says so:

Unsupported action: tapp — not executed. A declared step must run or fail, never silently skip.

Author from this table or, better, from the live catalog: GET /api/mobile-tests/actions. It drives autocomplete in the test editor, and a build-time check keeps it, the validator and the agent's dispatch switch in agreement — so an action you can discover is one that runs.

ActionAliasesWhat it doesRequired fields
launch_appLaunch / activate the app under test
close_appTerminate the app
tapclickTap the elementselector
typesend_keysClear the field, then type into itselector, value
clearClear a text fieldselector
long_pressPress-and-hold the elementselector
hide_keyboardHide the soft keyboard if shown
swipe_upSwipe up
swipe_downSwipe down
swipe_leftSwipe left
swipe_rightSwipe right
scroll_to_elementScroll until the element is visibleselector
navigate_backDevice back button
wait_for_elementWait until the element appearsselector, timeout
wait_for_visibleWait until the element is visibleselector, timeout
wait_for_invisibleWait until the element is gone (or was never there)selector, timeout
wait_for_clickablewait_for_tappableWait until it can actually be tapped, not merely drawnselector, timeout
wait_for_textWait until the element's text contains the expected valueselector, expected, timeout
double_tapdouble_clickTwo taps in quick successionselector
waitwait_seconds, sleepPause. Prefer a real wait above — a fixed pause is slow when too long and flaky when too shorttimeout
validate_textassert_textAssert element text contains the expected valueselector, expected
validate_containsassert_containsSame as above, explicit containsselector, expected
validate_visibleassert_visibleFail if element not displayedselector
validate_not_visibleassert_not_visibleFail if element IS displayedselector
validate_enabledassert_enabledFail if element not enabledselector
get_textRead the element's text (logged)selector
take_screenshotCapture the screen

value vs expected

  • value — data for input actions (type)
  • expected — assertion text for validation actions (validate_text, validate_contains). expectedValue is accepted as an alias and takes precedence when both are set.

validate_text uses contains matching, not equals — consistent with UI testing. Scope the selector tighter if you need a stricter check.

Swiping — the direction goes in value, never in direction

There is no direction: field. swipe exists and reads the direction from value:

- action: swipe
value: down # up | down | left | right — defaults to up

The four explicit actions do the same thing and read better:

- action: swipe_down
- action: swipe_up
- action: swipe_left
- action: swipe_right

Writing direction: down does not fail — the field is ignored and the swipe defaults to up, so the test does something other than what it says. Prefer the explicit actions.

Webviews — seeing inside a hybrid app

If part of your app is a web page — a React Native web screen, Cordova, an in-app browser, an embedded checkout or consent page — its elements are not in the native tree. A locator that is obviously right when you look at the screen finds nothing, and that looks exactly like a bad selector.

Switch context first:

- action: wait_for_visible # make sure the screen hosting it has loaded
selector: "id:checkout_container"
- action: switch_context
value: WEBVIEW # first webview; NATIVE_APP goes back
- action: tap
selector: "css:#pay-now" # now a web selector works
- action: switch_context
value: NATIVE_APP

WEBVIEW matches the first webview by prefix, so you do not have to write the package-suffixed name the driver actually reports. list_contexts prints everything available — useful once, while writing the test.

A webview that is still loading is not in the list yet

The refusal names what is available:

No context matching 'WEBVIEW'. Available: [NATIVE_APP]. A webview that is still loading
is not listed yet — wait for an element on the screen that hosts it before switching.

Waiting for a native element on the hosting screen first is the fix, as in the example above.

The app and the device

ActionWhat it doesRequired fields
switch_contextMove between native and webviewvalue
list_contextsLog every context available right now
open_deep_linkOpen the app straight at a screenvalue (the link); Android also options.package
background_appSend it away for N seconds, then bring it backtimeout (seconds, default 5)
activate_appBring an app to the front, starting it if neededvalue (package / bundle id)
terminate_appStop an appvalue (package / bundle id)
rotatePORTRAIT or LANDSCAPEvalue
press_keyA hardware or keyboard key by namevalue (e.g. ENTER, BACK, SEARCH)
open_notificationsPull down the notification shade (Android)

Deep links are the biggest lever on a mobile suite. Without one, every test walks the app from launch, so it is slow and it breaks when navigation changes — even when navigation is not what it is testing.

- action: open_deep_link
value: "myapp://orders/1234"

Backgrounding is how you test session expiry and state restoration:

- action: background_app
timeout: 30
- action: validate_visible
selector: "id:login_screen" # did it ask us to sign in again?

press_key with ENTER matters more than it sounds: plenty of screens only submit when the keyboard's own return key is pressed, and there is no button to tap instead.

Android-only

press_key and open_notifications are Android capabilities. On an iOS session they fail with a message saying so, rather than doing nothing.

timeout units

wait and the wait_for_* actions read timeout. A value at or below 600 is treated as seconds, anything larger as milliseconds — so timeout: 5 and timeout: 5000 both mean five seconds. This is the same rule web tests use.

- action: wait
timeout: 5 # 5 seconds
- action: wait_for_element
selector: "accessibility:test-PRODUCTS"
timeout: 10000 # 10 seconds
This changed on 2026-08-18

Mobile used to stop at 100, while web stopped at 600. So timeout: 300 was five minutes in a web test and three tenths of a second here.

The failure was quiet, which is what made it costly: a wait that collapses to a fraction of a second reports that it could not find the element, so it looks like a bad selector rather than a bad timeout. Somebody rewrites a locator that was fine all along.

Both now use 600. If you wrote a mobile timeout between 101 and 600 to work around the old behaviour, it now means seconds — check it still says what you meant.

See Web UI YAML → timeout units.

Variables

Same {{variable}} and ${variable} syntax as the rest of the platform (powered by Apache Commons Text StringSubstitutor).

steps:
- action: type
selector: accessibility:test-Username
value: "{{username}}"
- action: type
selector: accessibility:test-Password
value: "{{password}}"

Values come from the environment's auth config or per-test variables. Never hardcode credentials in the YAML — see Credentials.

Backend conditions

Beside steps:, a test can declare a top-level mocks: block naming the backend responses it needs for that run — an expired subscription, a 500, an empty list. See Mocking the Backend.

mocks:
- method: GET
path: /v1/subscription
status: 200
body: '{"tier":"expired"}'
steps:
- action: tap
selector: "accessibility:test-Account"

Capabilities

The platform builds the Appium capability dict from the mobile-app's config + the device tier. You can extend it via:

Per-app extra capabilities

In the mobile app's config (UI: Mobile Apps → Edit → Appium Capabilities):

{
"appium:autoGrantPermissions": true,
"appium:noReset": false,
"appium:newCommandTimeout": 120
}

Merged on top of platform defaults.

Vendor-specific options

For BrowserStack/Sauce Labs, the platform builds bstack:options / sauce:options automatically from credentials + cloud_config. You can override individual keys via:

{
"bstackOptions": {
"debug": true,
"networkLogs": false,
"buildName": "release-v3.2"
}
}

These get nested under bstack:options in the final capability dict.

Common patterns

Login flow

steps:
- action: type
selector: accessibility:username-field
value: "{{username}}"
- action: type
selector: accessibility:password-field
value: "{{password}}"
- action: tap
selector: accessibility:login-button
- action: wait_for_element
selector: accessibility:home-screen
timeout: 15000

Scroll-and-find

steps:
- action: scroll_to_element
selector: accessibility:settings-link
- action: tap
selector: accessibility:settings-link

Form input with validation

steps:
- action: clear
selector: accessibility:email-field
- action: type
selector: accessibility:email-field
value: "invalid"
- action: tap
selector: accessibility:submit-button
- action: assert_visible
selector: accessibility:email-error
- action: assert_text
selector: accessibility:email-error
expectedText: "valid email"

What's different from UI testing

UI testsMobile tests
EnginePlaywright / Selenium / HtmlUnitAppium (UiAutomator2 / XCUITest)
Selector syntaxCSS / XPathaccessibility: / id: / xpath: (Appium-style)
home / back actionsN/ANative nav
swiperarecommon
App contextURL-basedPackage or pre-installed

Element discovery

Use Inspect Elements on the mobile app's detail page to crawl screens and capture available accessibility IDs. Saves accidental selector drift when the app's resource IDs change.

Tips

  1. Lean on accessibility IDs — they survive UI redesigns.
  2. Use wait_for_element aggressively — mobile UIs animate, and tap on an off-screen element fails. wait_for_element makes tests stable.
  3. Keep tests short — < 20 steps per test. Long tests are harder to debug.
  4. Tag step results with screenshots — adds action: take_screenshot after critical state transitions. Auto-attached to the run result for triage.
  • Local emulator — set up Appium locally to develop tests
  • Device farms — run the same YAML on real BrowserStack devices
  • Credentials{{username}} / {{password}} come from the vault