E2E Testing — Mobile (Expo / React Native)
Drive native mobile end-to-end tests through Maestro and Maestro MCP.
The user writes (or approves) a Markdown feature spec; an agent emits a
Maestro YAML flow, runs it against a simulator or Maestro Cloud, and
self-heals when locators drift.
This is the mobile counterpart to e2e-testing.
Anything orthogonal to native (web flows, WebViews inside a hybrid app)
defers to that skill.
The two skills compose; they do not overlap.
This SKILL.md is a thin index.
Decision rules live in rules/*.md and load on demand.
Worked references (Maestro CLI surface, MCP tool catalog, EAS Workflow
wiring, Detox legacy notes, mobile pyramid math) live in
references/*.md.
Literal boilerplate the skill emits lives in
templates/*.md.
Do not preload everything — load only what the current phase asks for.
When to use
Reach for this skill when any of the following is true:
- An Expo or React Native app needs E2E coverage of a native user flow.
- A bug only repros across native navigation, deep links, push,
permissions prompts, or a real device sensor.
- A flake needs a Maestro heal pass instead of a manual locator hunt.
- The repo has no
.maestro/ flows or EAS E2E build profile yet and
needs Phase 0 setup.
Do not reach for this skill when:
- A unit or component test would catch the same bug — defer to
tdd and the layer rule in
rules/layer-decision.md.
- The flow is browser-only — defer to
e2e-testing.
- The flow lives entirely inside a WebView in a hybrid app — defer to
e2e-testing, which automates the WebView
via Playwright while Maestro handles native chrome around it.
- The change is a pure refactor with no behavioural surface.
Phase 0 — Preflight (mandatory gate)
Before any agent loop, verify the repo is wired for Maestro on Expo / RN.
Halt and ask the user before installing anything or producing builds.
Run these checks (read-only):
# 1. Maestro CLI installed?
maestro --version 2>/dev/null
# 2. Flow directory present?
ls -d .maestro 2>/dev/null
# 3. EAS build profile for E2E exists?
jq '.build | has("e2e")' eas.json 2>/dev/null
# 4. Simulator / emulator available?
xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone'
adb devices 2>/dev/null
# 5. Existing Detox install (legacy escape hatch)?
jq '.devDependencies | has("detox")' package.json 2>/dev/null
Decision table:
| State |
Action |
Maestro CLI present + .maestro/ exists + eas.json e2e profile |
Proceed to Phase 1. |
| Maestro CLI missing |
Halt. Print install plan (templates/install-plan.md). Ask permission. |
.maestro/ missing |
Halt. Propose creating .maestro/ with the templates/flow.yaml starter. Ask first. |
eas.json e2e build profile missing |
Halt. Propose templates/eas-build-profile.json. Ask first. |
| No simulator / emulator running |
Halt. Ask the user to boot one, or proceed with Maestro Cloud only. |
| Existing Detox suite detected |
Note it. Read references/detox-legacy.md before proposing migration. |
Bare RN project (no expo / no eas.json) |
Skip EAS-specific checks. Use Maestro CLI directly against a local build. |
Print the exact commands; do not run them silently.
The full install plan template is in templates/install-plan.md.
Phase 1 — Spec-first feature flow
The agent loop is spec → emit-flow → run → heal.
The spec is human-readable Markdown; the executable artefact is a
Maestro YAML flow.
Full rules: rules/spec-first-flow.md.
specs/<flow>.md ─┐
├─→ emit ─→ .maestro/<flow>.yaml ─→ run ─→ pass?
│ │ no
│ ▼
└───────────────── heal ←────────────── failing flow + trace
│
▼
patched flow or `testID` source-diff proposal
Two entry points:
- Spec already drafted by the user.
Skip exploration; emit the flow from
specs/<flow>.md.
- App exists, no spec yet.
Run an exploratory pass against a running build (simulator) and
draft
specs/<flow>.md.
The user reviews the Markdown before flow emission.
Use the Markdown template in
../e2e-testing/templates/spec.md.
The spec format is identical across web and mobile — share it.
Locator ladder for React Native (when generating or healing)
The Maestro flow walks the platform accessibility tree.
Pick locators in this order — never skip a rung:
id: <testID> — the source-of-truth selector for E2E.
Mapped to accessibilityIdentifier on iOS and resource-id on
Android (RN 0.64+). Stable across i18n and refactors.
text: <visible string> — only when the text is short, unique on
screen, and not localised.
accessibilityText: <label> — last resort, with caveats.
See rules/locator-strategy.md for
why accessibilityLabel should not double as a test selector.
testID is the standard fix, not an escape hatch.
When the Healer cannot find a stable element at rung 1, propose a
source diff that adds testID to the component (use the
setTestId helper in
templates/testid-helper.tsx to keep
iOS and Android consistent), and offer the diff for user approval
before patching the flow.
Full rules: rules/locator-strategy.md.
Phase 2 — Token-aware execution
Maestro flows are YAML, not pixel snapshots — the per-step token cost
is already an order of magnitude below screenshot-driven runners.
Defaults the skill prescribes
(full rules: rules/token-budget.md):
- Run only the changed flow on iteration:
maestro test .maestro/<flow>.yaml.
- Use
--shards only on Maestro Cloud, never on local iteration.
record_screen: false by default; flip to true only when chasing a
visual race.
retries: 1 for local, retries: 2 for Maestro Cloud.
- Run the Healer only on failure, not on every save.
- Reuse a cached signed-in
app.app / app.apk from the EAS E2E
build profile — never rebuild on every flow run.
- Cap the heal loop at three attempts per failing flow before
escalating.
Phase 3 — Verification
After the agent emits a flow:
- Run the flow once against a simulator or device.
It must pass on first run, or the Healer must converge in ≤ 3 attempts.
- Invoke
test-provenance-guard on
any TypeScript helpers the flow imports (e.g. fixture builders) to
ensure they call production code instead of a private re-implementation.
- Open
.github/workflows/eas-e2e.yml (or the EAS Workflow YAML) and
confirm the maestro-cloud job is wired with retries: 2 and
record_screen: false per
templates/eas-workflow.yaml.
If the heal loop fails to converge:
- Invoke
confidence(analysis) on the flow failure.
- If confidence is below 90%, escalate to the user with the Maestro
log, the spec, and the proposed locator changes — do not keep
healing blindly.
Decision flow at a glance
| Signal |
Do |
| Bug fixable by a unit or component test |
Use tdd, not this skill. |
| Flow is browser-only or pure web |
Use e2e-testing, not this skill. |
| Flow lives inside a WebView in a hybrid RN app |
Pair with e2e-testing for the WebView; Maestro for the native chrome. |
| Multi-screen native flow, deep links, permissions, or push |
Spec-first feature flow (Phase 1). |
| Flaky existing flow |
Healer pass only; do not rewrite without spec context. |
Locator unstable, no stable testID |
Propose testID diff via the setTestId helper. |
Repo missing Maestro CLI or .maestro/ or eas.json E2E profile |
Phase 0 halt + ask permission. |
| Heal loop > 3 attempts |
Stop, run confidence(analysis), escalate. |
| Flow passes on first run, never seen failing |
Verify any imported helpers via test-provenance-guard before declaring done. |
| Existing Detox suite is green and stable |
Keep it; see references/detox-legacy.md. |
| Detox suite is brittle through RN upgrades |
Migrate flow-by-flow to Maestro; do not rewrite the whole suite at once. |
Composes with
e2e-testing — owns web E2E and the
WebView half of hybrid mobile apps. This skill defers to it for
anything browser-shaped.
tdd — owns the unit and component layers
(Jest + React Native Testing Library). This skill defers for
anything below E2E.
test-provenance-guard — runs
on TypeScript helpers imported by flows to catch tests-by-construction.
confidence — gate when the heal loop fails.
holistic-analysis — if a flow is
failing for reasons no flow rewrite can fix, step back instead of
patching.
ux — review accessibility labels separately from
test IDs (the two should never share a value).
References
references/maestro-cli.md — CLI surface
(launchApp, tapOn, assertVisible, runFlow, Studio, doctor).
references/maestro-mcp.md — Maestro
MCP tools for agent-driven flow generation and healing.
references/eas-workflows.md —
eas.json E2E profile, .maestro/ layout, maestro-cloud job
parameters (build_id, flow_path, shards, retries,
record_screen, device_identifier).
references/detox-legacy.md — when
to keep an existing Detox suite vs. migrate.
references/pyramid-mobile.md —
pyramid math for Expo / RN with Jest, React Native Testing Library,
and Maestro.
Templates
- Writing E2E for logic a unit or RNTL component test catches.
- Reusing
accessibilityLabel as the test selector (it is for screen
readers; doubling its purpose breaks accessibility).
- Patching the flow with absolute coordinates (
tapOn: { point: "50%, 80%" })
instead of proposing a testID diff.
- Running the Healer on every save, or on a passing flow.
- Rebuilding the
.app / .apk on every flow run.
- Generating flows against a stub server, not the real app stack.
- Migrating a stable Detox suite all at once instead of flow-by-flow.
Definition of done
1---2name: e2e-testing-mobile3description: Plans, generates, runs, and heals end-to-end tests for Expo and React Native mobile apps using Maestro (the 2026 standard for RN E2E, adopted by Meta, Microsoft, and DoorDash, and integrated with Expo via EAS Workflows). Drives a spec-first YAML-flow loop, proposes `testID` source diffs (never `accessibilityLabel` reuse), runs Maestro Cloud as an EAS job, and stays token-aware via `--shards`, `--retries`, and failure-only healing. Use for native flows in Expo / RN apps. Triggers on "test this RN flow", "add mobile e2e", "maestro flow", "expo e2e", "e2e for react native", "test the native app", "/e2e-testing-mobile". Defer to [`e2e-testing`](../e2e-testing/SKILL.md) for web flows and the WebView portion of hybrid apps.4license: MIT5---67# E2E Testing — Mobile (Expo / React Native)89Drive native mobile end-to-end tests through Maestro and Maestro MCP.10The user writes (or approves) a Markdown feature spec; an agent emits a11Maestro YAML flow, runs it against a simulator or Maestro Cloud, and12self-heals when locators drift.1314This is the **mobile** counterpart to [`e2e-testing`](../e2e-testing/SKILL.md).15Anything orthogonal to native (web flows, WebViews inside a hybrid app)16defers to that skill.17The two skills compose; they do not overlap.1819> **This `SKILL.md` is a thin index.**20> Decision rules live in [`rules/*.md`](./rules) and load on demand.21> Worked references (Maestro CLI surface, MCP tool catalog, EAS Workflow22> wiring, Detox legacy notes, mobile pyramid math) live in23> [`references/*.md`](./references).24> Literal boilerplate the skill emits lives in25> [`templates/*.md`](./templates).26> Do not preload everything — load only what the current phase asks for.2728---2930## When to use3132Reach for this skill when any of the following is true:3334- An Expo or React Native app needs E2E coverage of a native user flow.35- A bug only repros across native navigation, deep links, push,36 permissions prompts, or a real device sensor.37- A flake needs a Maestro heal pass instead of a manual locator hunt.38- The repo has no `.maestro/` flows or EAS E2E build profile yet and39 needs Phase 0 setup.4041Do **not** reach for this skill when:4243- A unit or component test would catch the same bug — defer to44 [`tdd`](../../quality/tdd/SKILL.md) and the layer rule in45 [`rules/layer-decision.md`](./rules/layer-decision.md).46- The flow is browser-only — defer to47 [`e2e-testing`](../e2e-testing/SKILL.md).48- The flow lives entirely inside a WebView in a hybrid app — defer to49 [`e2e-testing`](../e2e-testing/SKILL.md), which automates the WebView50 via Playwright while Maestro handles native chrome around it.51- The change is a pure refactor with no behavioural surface.5253---5455## Phase 0 — Preflight (mandatory gate)5657Before any agent loop, verify the repo is wired for Maestro on Expo / RN.58Halt and ask the user before installing anything or producing builds.5960Run these checks (read-only):6162```bash63# 1. Maestro CLI installed?64maestro --version 2>/dev/null6566# 2. Flow directory present?67ls -d .maestro 2>/dev/null6869# 3. EAS build profile for E2E exists?70jq '.build | has("e2e")' eas.json 2>/dev/null7172# 4. Simulator / emulator available?73xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone'74adb devices 2>/dev/null7576# 5. Existing Detox install (legacy escape hatch)?77jq '.devDependencies | has("detox")' package.json 2>/dev/null78```7980Decision table:8182| State | Action |83| ----------------------------------------------------------- | --------------------------------------------------------------------------- |84| Maestro CLI present + `.maestro/` exists + `eas.json` `e2e` profile | Proceed to Phase 1. |85| Maestro CLI missing | **Halt.** Print install plan ([`templates/install-plan.md`](./templates/install-plan.md)). Ask permission. |86| `.maestro/` missing | **Halt.** Propose creating `.maestro/` with the [`templates/flow.yaml`](./templates/flow.yaml) starter. Ask first. |87| `eas.json` `e2e` build profile missing | **Halt.** Propose [`templates/eas-build-profile.json`](./templates/eas-build-profile.json). Ask first. |88| No simulator / emulator running | **Halt.** Ask the user to boot one, or proceed with Maestro Cloud only. |89| Existing Detox suite detected | Note it. Read [`references/detox-legacy.md`](./references/detox-legacy.md) before proposing migration. |90| Bare RN project (no `expo` / no `eas.json`) | Skip EAS-specific checks. Use Maestro CLI directly against a local build. |9192Print the exact commands; do not run them silently.93The full install plan template is in [`templates/install-plan.md`](./templates/install-plan.md).9495---9697## Phase 1 — Spec-first feature flow9899The agent loop is **spec → emit-flow → run → heal**.100The spec is human-readable Markdown; the executable artefact is a101Maestro YAML flow.102Full rules: [`rules/spec-first-flow.md`](./rules/spec-first-flow.md).103104```105specs/<flow>.md ─┐106 ├─→ emit ─→ .maestro/<flow>.yaml ─→ run ─→ pass?107 │ │ no108 │ ▼109 └───────────────── heal ←────────────── failing flow + trace110 │111 ▼112 patched flow or `testID` source-diff proposal113```114115Two entry points:1161171. **Spec already drafted by the user.**118 Skip exploration; emit the flow from `specs/<flow>.md`.1192. **App exists, no spec yet.**120 Run an exploratory pass against a running build (simulator) and121 draft `specs/<flow>.md`.122 The user reviews the Markdown before flow emission.123124Use the Markdown template in125[`../e2e-testing/templates/spec.md`](../e2e-testing/templates/spec.md).126The spec format is identical across web and mobile — share it.127128### Locator ladder for React Native (when generating or healing)129130The Maestro flow walks the platform accessibility tree.131Pick locators in this order — never skip a rung:1321331. `id: <testID>` — the source-of-truth selector for E2E.134 Mapped to `accessibilityIdentifier` on iOS and `resource-id` on135 Android (RN 0.64+). Stable across i18n and refactors.1362. `text: <visible string>` — only when the text is short, unique on137 screen, and not localised.1383. `accessibilityText: <label>` — last resort, with caveats.139 See [`rules/locator-strategy.md`](./rules/locator-strategy.md) for140 why `accessibilityLabel` should **not** double as a test selector.141142`testID` is the standard fix, not an escape hatch.143When the Healer cannot find a stable element at rung 1, propose a144**source diff** that adds `testID` to the component (use the145`setTestId` helper in146[`templates/testid-helper.tsx`](./templates/testid-helper.tsx) to keep147iOS and Android consistent), and offer the diff for user approval148before patching the flow.149Full rules: [`rules/locator-strategy.md`](./rules/locator-strategy.md).150151---152153## Phase 2 — Token-aware execution154155Maestro flows are YAML, not pixel snapshots — the per-step token cost156is already an order of magnitude below screenshot-driven runners.157Defaults the skill prescribes158(full rules: [`rules/token-budget.md`](./rules/token-budget.md)):159160- Run only the changed flow on iteration: `maestro test .maestro/<flow>.yaml`.161- Use `--shards` only on Maestro Cloud, never on local iteration.162- `record_screen: false` by default; flip to `true` only when chasing a163 visual race.164- `retries: 1` for local, `retries: 2` for Maestro Cloud.165- Run the Healer **only on failure**, not on every save.166- Reuse a cached signed-in `app.app` / `app.apk` from the EAS E2E167 build profile — never rebuild on every flow run.168- Cap the heal loop at three attempts per failing flow before169 escalating.170171---172173## Phase 3 — Verification174175After the agent emits a flow:1761771. Run the flow once against a simulator or device.178 It must pass on first run, or the Healer must converge in ≤ 3 attempts.1792. Invoke [`test-provenance-guard`](../../quality/test-provenance-guard/SKILL.md) on180 any TypeScript helpers the flow imports (e.g. fixture builders) to181 ensure they call production code instead of a private re-implementation.1823. Open `.github/workflows/eas-e2e.yml` (or the EAS Workflow YAML) and183 confirm the `maestro-cloud` job is wired with `retries: 2` and184 `record_screen: false` per185 [`templates/eas-workflow.yaml`](./templates/eas-workflow.yaml).186187If the heal loop fails to converge:188189- Invoke `confidence(analysis)` on the flow failure.190- If confidence is below 90%, escalate to the user with the Maestro191 log, the spec, and the proposed locator changes — do **not** keep192 healing blindly.193194---195196## Decision flow at a glance197198| Signal | Do |199| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- |200| Bug fixable by a unit or component test | Use [`tdd`](../../quality/tdd/SKILL.md), not this skill. |201| Flow is browser-only or pure web | Use [`e2e-testing`](../e2e-testing/SKILL.md), not this skill. |202| Flow lives inside a WebView in a hybrid RN app | Pair with [`e2e-testing`](../e2e-testing/SKILL.md) for the WebView; Maestro for the native chrome. |203| Multi-screen native flow, deep links, permissions, or push | Spec-first feature flow (Phase 1). |204| Flaky existing flow | Healer pass only; do not rewrite without spec context. |205| Locator unstable, no stable `testID` | Propose `testID` diff via the `setTestId` helper. |206| Repo missing Maestro CLI or `.maestro/` or `eas.json` E2E profile | Phase 0 halt + ask permission. |207| Heal loop > 3 attempts | Stop, run `confidence(analysis)`, escalate. |208| Flow passes on first run, never seen failing | Verify any imported helpers via `test-provenance-guard` before declaring done. |209| Existing Detox suite is green and stable | Keep it; see [`references/detox-legacy.md`](./references/detox-legacy.md). |210| Detox suite is brittle through RN upgrades | Migrate flow-by-flow to Maestro; do not rewrite the whole suite at once. |211212---213214## Composes with215216- [`e2e-testing`](../e2e-testing/SKILL.md) — owns web E2E and the217 WebView half of hybrid mobile apps. This skill defers to it for218 anything browser-shaped.219- [`tdd`](../../quality/tdd/SKILL.md) — owns the unit and component layers220 (Jest + React Native Testing Library). This skill defers for221 anything below E2E.222- [`test-provenance-guard`](../../quality/test-provenance-guard/SKILL.md) — runs223 on TypeScript helpers imported by flows to catch tests-by-construction.224- [`confidence`](../../quality/confidence/SKILL.md) — gate when the heal loop fails.225- [`holistic-analysis`](../../analysis/holistic-analysis/SKILL.md) — if a flow is226 failing for reasons no flow rewrite can fix, step back instead of227 patching.228- [`ux`](../../design/ux/SKILL.md) — review accessibility labels separately from229 test IDs (the two should never share a value).230231---232233## References234235- [`references/maestro-cli.md`](./references/maestro-cli.md) — CLI surface236 (`launchApp`, `tapOn`, `assertVisible`, `runFlow`, Studio, doctor).237- [`references/maestro-mcp.md`](./references/maestro-mcp.md) — Maestro238 MCP tools for agent-driven flow generation and healing.239- [`references/eas-workflows.md`](./references/eas-workflows.md) —240 `eas.json` E2E profile, `.maestro/` layout, `maestro-cloud` job241 parameters (`build_id`, `flow_path`, `shards`, `retries`,242 `record_screen`, `device_identifier`).243- [`references/detox-legacy.md`](./references/detox-legacy.md) — when244 to keep an existing Detox suite vs. migrate.245- [`references/pyramid-mobile.md`](./references/pyramid-mobile.md) —246 pyramid math for Expo / RN with Jest, React Native Testing Library,247 and Maestro.248249## Templates250251- [`templates/install-plan.md`](./templates/install-plan.md) — Phase 0252 halt message with the exact commands to install Maestro + EAS CLI253 and scaffold `.maestro/`.254- [`templates/flow.yaml`](./templates/flow.yaml) — sample Maestro flow255 with `launchApp`, `tapOn` (`id:` and `text:` forms), `assertVisible`,256 and `runFlow` includes.257- [`templates/eas-workflow.yaml`](./templates/eas-workflow.yaml) — EAS258 Workflow with a build job and a `maestro-cloud` job.259- [`templates/eas-build-profile.json`](./templates/eas-build-profile.json)260 — `eas.json` snippet for an `e2e` profile producing `.apk` and `.app`.261- [`templates/testid-helper.tsx`](./templates/testid-helper.tsx) —262 `setTestId` helper that maps `testID` and `accessibilityLabel`263 correctly per platform without conflating the two.264265---266267## Anti-patterns (one-liner — full list in [`rules/anti-patterns.md`](./rules/anti-patterns.md))268269- Writing E2E for logic a unit or RNTL component test catches.270- Reusing `accessibilityLabel` as the test selector (it is for screen271 readers; doubling its purpose breaks accessibility).272- Patching the flow with absolute coordinates (`tapOn: { point: "50%, 80%" }`)273 instead of proposing a `testID` diff.274- Running the Healer on every save, or on a passing flow.275- Rebuilding the `.app` / `.apk` on every flow run.276- Generating flows against a stub server, not the real app stack.277- Migrating a stable Detox suite all at once instead of flow-by-flow.278279---280281## Definition of done282283- [ ] Phase 0 preflight passed or installs were user-approved.284- [ ] `specs/<flow>.md` exists and the user reviewed it.285- [ ] `.maestro/<flow>.yaml` passes against a real build.286- [ ] Any imported TS helpers pass `test-provenance-guard`.287- [ ] EAS Workflow `maestro-cloud` job runs the flow on Cloud with288 `retries: 2` and `record_screen: false`.289- [ ] If a `testID` was added, it is in the source diff and committed,290 with no `accessibilityLabel` reuse.