Mobile Automation
Mobile automation is slower, flakier, and far more environment-dependent than web automation. Syntaxis automates fewer cases on mobile deliberately, picks the highest-value flows, and leaves the rest manual without apology.
Set up a harness first with /tests mobile — see Testing Setup.
Choosing a harness
Section titled “Choosing a harness”| Stack | Default | Why |
|---|---|---|
| Flutter | integration_test |
Native to the toolchain, no extra service |
| React Native / Expo | Maestro | YAML flows, timing-tolerant, quick to set up |
| React Native with Detox already | Detox | Gray-box idle detection, fewer flakes |
| Ionic + Capacitor | Playwright on the web build | Most behaviour lives in the web layer |
| Native from shared code | Appium | The only genuine cross-platform native driver |
| Native iOS / Android with existing suites | XCUITest / Espresso | Fastest and least flaky per platform |
Maestro is preferred over Appium for a new React Native harness: no server, no driver matrix, far less configuration for the same smoke coverage.
For Ionic, the web harness comes first. Appium is added only when the plan targets plugin behaviour, permissions, deep links, or store builds — and the web harness is never described as proving the native shell.
Selector policy
Section titled “Selector policy”Use the platform’s accessibility identifier. It is stable, it is what assistive technology uses, and a missing one is a real accessibility defect worth reporting.
| Platform | Set with | Query by |
|---|---|---|
| React Native | testID |
testID or accessibility label |
| Flutter | Key("..."), Semantics(identifier:) |
find.byKey, find.bySemanticsLabel |
| iOS native | accessibilityIdentifier |
identifier |
| Android native | resource-id, contentDescription |
resource id |
Never select by screen coordinates, list index, or a translated string. Coordinates break on every screen size, and a translated string breaks the suite the moment localization lands.
Maestro
Section titled “Maestro”appId: com.example.app---- launchApp: clearState: true- tapOn: "Cart"- tapOn: "Checkout"- tapOn: id: "card-4242"- tapOn: "Place order"# The expected result, both halves.- assertVisible: ".*expired.*"- assertVisible: id: "cart-item"One flow file per case, named with the case ID. clearState: true keeps flows
independent. Retries are built into the commands, so there is no sleep to add.
it("TC-021 rejects an expired card", async () => { await device.launchApp({ newInstance: true }); await element(by.id("cart-tab")).tap(); await element(by.id("checkout")).tap(); await element(by.id("card-4242")).tap(); await element(by.id("place-order")).tap();
await expect(element(by.text(/expired/))).toBeVisible(); await expect(element(by.id("cart-item"))).toBeVisible();});Detox needs a build of the app before the run; that build step is part of the
documented command. Prefer by.id over by.text so the suite survives copy
changes.
Flutter integration_test
Section titled “Flutter integration_test”testWidgets('TC-021 rejects an expired card', (tester) async { app.main(); await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('checkout'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('card-4242'))); await tester.tap(find.byKey(const Key('place-order'))); await tester.pumpAndSettle();
expect(find.textContaining('expired'), findsOneWidget); expect(find.byKey(const Key('cart-item')), findsOneWidget);});pumpAndSettle is the correct wait, not a sleep — but it hangs on an infinite
animation such as a looping spinner, where a fixed number of pumped frames is
right instead.
Widget tests are faster and cover most logic. integration_test is reserved for
cases that genuinely need the whole app.
Appium
Section titled “Appium”The cross-platform option, and the only one driving native iOS and Android from one suite. It needs an Appium server plus the platform driver (XCUITest, UiAutomator2) and is the slowest of the options, so the automated set stays small and high-value.
Environment reality
Section titled “Environment reality”A mobile result means nothing without its environment. Every cycle records, and every report states:
- simulator or emulator, versus a real device
- which OS versions
- whether a real payment, push notification, biometric, or camera path was exercised or stubbed
A green suite on one simulator does not prove the case on the target device matrix. Syntaxis says what was covered and what was not rather than implying the matrix passed.
Proving a test can fail
Section titled “Proving a test can fail”Required, and it matters more here than on web: a flow that taps a missing element often reports a generic timeout that looks like a slow device. Invert the assertion, run the single flow, confirm it fails for the expected reason, then restore it.
Artifacts
Section titled “Artifacts”Device logs, screenshots, recordings, .app and .apk builds, signing material,
and stored credentials all stay out of Git.
Device suites are not added to Verify or CI without a separate request. CI
runners usually have no simulator at all.