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.mdis a thin index. Decision rules live inrules/*.mdand load on demand. Worked references (Maestro CLI surface, MCP tool catalog, EAS Workflow wiring, Detox legacy notes, mobile pyramid math) live inreferences/*.md. Literal boilerplate the skill emits lives intemplates/*.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
tddand the layer rule inrules/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 toaccessibilityIdentifieron iOS andresource-idon 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. Seerules/locator-strategy.mdfor whyaccessibilityLabelshould 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
--shardsonly on Maestro Cloud, never on local iteration. record_screen: falseby default; flip totrueonly when chasing a visual race.retries: 1for local,retries: 2for Maestro Cloud.- Run the Healer only on failure, not on every save.
- Reuse a cached signed-in
app.app/app.apkfrom 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-guardon 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 themaestro-cloudjob is wired withretries: 2andrecord_screen: falsepertemplates/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.jsonE2E profile,.maestro/layout,maestro-cloudjob 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
templates/install-plan.md— Phase 0 halt message with the exact commands to install Maestro + EAS CLI and scaffold.maestro/.templates/flow.yaml— sample Maestro flow withlaunchApp,tapOn(id:andtext:forms),assertVisible, andrunFlowincludes.templates/eas-workflow.yaml— EAS Workflow with a build job and amaestro-cloudjob.templates/eas-build-profile.json—eas.jsonsnippet for ane2eprofile producing.apkand.app.templates/testid-helper.tsx—setTestIdhelper that mapstestIDandaccessibilityLabelcorrectly per platform without conflating the two.
Anti-patterns (one-liner — full list in rules/anti-patterns.md)
- Writing E2E for logic a unit or RNTL component test catches.
- Reusing
accessibilityLabelas 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 atestIDdiff. - Running the Healer on every save, or on a passing flow.
- Rebuilding the
.app/.apkon 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
- Phase 0 preflight passed or installs were user-approved.
-
specs/<flow>.mdexists and the user reviewed it. -
.maestro/<flow>.yamlpasses against a real build. - Any imported TS helpers pass
test-provenance-guard. - EAS Workflow
maestro-cloudjob runs the flow on Cloud withretries: 2andrecord_screen: false. - If a
testIDwas added, it is in the source diff and committed, with noaccessibilityLabelreuse.