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
Source: mthines/agent-skills — distributed by TomeVault.
1---2name: mthines-agent-skills-e2e-testing-mobile3description: E2E Testing — Mobile (Expo / React Native)4---56# E2E Testing — Mobile (Expo / React Native)78Drive native mobile end-to-end tests through Maestro and Maestro MCP.9The user writes (or approves) a Markdown feature spec; an agent emits a10Maestro YAML flow, runs it against a simulator or Maestro Cloud, and11self-heals when locators drift.1213This is the **mobile** counterpart to [`e2e-testing`](../e2e-testing/SKILL.md).14Anything orthogonal to native (web flows, WebViews inside a hybrid app)15defers to that skill.16The two skills compose; they do not overlap.1718> **This `SKILL.md` is a thin index.**19> Decision rules live in [`rules/*.md`](./rules) and load on demand.20> Worked references (Maestro CLI surface, MCP tool catalog, EAS Workflow21> wiring, Detox legacy notes, mobile pyramid math) live in22> [`references/*.md`](./references).23> Literal boilerplate the skill emits lives in24> [`templates/*.md`](./templates).25> Do not preload everything — load only what the current phase asks for.2627---2829## When to use3031Reach for this skill when any of the following is true:3233- An Expo or React Native app needs E2E coverage of a native user flow.34- A bug only repros across native navigation, deep links, push,35 permissions prompts, or a real device sensor.36- A flake needs a Maestro heal pass instead of a manual locator hunt.37- The repo has no `.maestro/` flows or EAS E2E build profile yet and38 needs Phase 0 setup.3940Do **not** reach for this skill when:4142- A unit or component test would catch the same bug — defer to43 [`tdd`](../../quality/tdd/SKILL.md) and the layer rule in44 [`rules/layer-decision.md`](./rules/layer-decision.md).45- The flow is browser-only — defer to46 [`e2e-testing`](../e2e-testing/SKILL.md).47- The flow lives entirely inside a WebView in a hybrid app — defer to48 [`e2e-testing`](../e2e-testing/SKILL.md), which automates the WebView49 via Playwright while Maestro handles native chrome around it.50- The change is a pure refactor with no behavioural surface.5152---5354## Phase 0 — Preflight (mandatory gate)5556Before any agent loop, verify the repo is wired for Maestro on Expo / RN.57Halt and ask the user before installing anything or producing builds.5859Run these checks (read-only):6061```bash62# 1. Maestro CLI installed?63maestro --version 2>/dev/null6465# 2. Flow directory present?66ls -d .maestro 2>/dev/null6768# 3. EAS build profile for E2E exists?69jq '.build | has("e2e")' eas.json 2>/dev/null7071# 4. Simulator / emulator available?72xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone'73adb devices 2>/dev/null7475# 5. Existing Detox install (legacy escape hatch)?76jq '.devDependencies | has("detox")' package.json 2>/dev/null77```7879Decision table:8081| State | Action |82| ----------------------------------------------------------- | --------------------------------------------------------------------------- |83| Maestro CLI present + `.maestro/` exists + `eas.json` `e2e` profile | Proceed to Phase 1. |84| Maestro CLI missing | **Halt.** Print install plan ([`templates/install-plan.md`](./templates/install-plan.md)). Ask permission. |85| `.maestro/` missing | **Halt.** Propose creating `.maestro/` with the [`templates/flow.yaml`](./templates/flow.yaml) starter. Ask first. |86| `eas.json` `e2e` build profile missing | **Halt.** Propose [`templates/eas-build-profile.json`](./templates/eas-build-profile.json). Ask first. |87| No simulator / emulator running | **Halt.** Ask the user to boot one, or proceed with Maestro Cloud only. |88| Existing Detox suite detected | Note it. Read [`references/detox-legacy.md`](./references/detox-legacy.md) before proposing migration. |89| Bare RN project (no `expo` / no `eas.json`) | Skip EAS-specific checks. Use Maestro CLI directly against a local build. |9091Print the exact commands; do not run them silently.92The full install plan template is in [`templates/install-plan.md`](./templates/install-plan.md).9394---9596## Phase 1 — Spec-first feature flow9798The agent loop is **spec → emit-flow → run → heal**.99The spec is human-readable Markdown; the executable artefact is a100Maestro YAML flow.101Full rules: [`rules/spec-first-flow.md`](./rules/spec-first-flow.md).102103```104specs/<flow>.md ─┐105 ├─→ emit ─→ .maestro/<flow>.yaml ─→ run ─→ pass?106 │ │ no107 │ ▼108 └───────────────── heal ←────────────── failing flow + trace109 │110 ▼111 patched flow or `testID` source-diff proposal112```113114Two entry points:1151161. **Spec already drafted by the user.**117 Skip exploration; emit the flow from `specs/<flow>.md`.1182. **App exists, no spec yet.**119 Run an exploratory pass against a running build (simulator) and120 draft `specs/<flow>.md`.121 The user reviews the Markdown before flow emission.122123Use the Markdown template in124[`../e2e-testing/templates/spec.md`](../e2e-testing/templates/spec.md).125The spec format is identical across web and mobile — share it.126127### Locator ladder for React Native (when generating or healing)128129The Maestro flow walks the platform accessibility tree.130Pick locators in this order — never skip a rung:1311321. `id: <testID>` — the source-of-truth selector for E2E.133 Mapped to `accessibilityIdentifier` on iOS and `resource-id` on134 Android (RN 0.64+). Stable across i18n and refactors.1352. `text: <visible string>` — only when the text is short, unique on136 screen, and not localised.1373. `accessibilityText: <label>` — last resort, with caveats.138 See [`rules/locator-strategy.md`](./rules/locator-strategy.md) for139 why `accessibilityLabel` should **not** double as a test selector.140141`testID` is the standard fix, not an escape hatch.142When the Healer cannot find a stable element at rung 1, propose a143**source diff** that adds `testID` to the component (use the144`setTestId` helper in145[`templates/testid-helper.tsx`](./templates/testid-helper.tsx) to keep146iOS and Android consistent), and offer the diff for user approval147before patching the flow.148Full rules: [`rules/locator-strategy.md`](./rules/locator-strategy.md).149150---151152## Phase 2 — Token-aware execution153154Maestro flows are YAML, not pixel snapshots — the per-step token cost155is already an order of magnitude below screenshot-driven runners.156Defaults the skill prescribes157(full rules: [`rules/token-budget.md`](./rules/token-budget.md)):158159- Run only the changed flow on iteration: `maestro test .maestro/<flow>.yaml`.160- Use `--shards` only on Maestro Cloud, never on local iteration.161- `record_screen: false` by default; flip to `true` only when chasing a162 visual race.163- `retries: 1` for local, `retries: 2` for Maestro Cloud.164- Run the Healer **only on failure**, not on every save.165- Reuse a cached signed-in `app.app` / `app.apk` from the EAS E2E166 build profile — never rebuild on every flow run.167- Cap the heal loop at three attempts per failing flow before168 escalating.169170---171172## Phase 3 — Verification173174After the agent emits a flow:1751761. Run the flow once against a simulator or device.177 It must pass on first run, or the Healer must converge in ≤ 3 attempts.1782. Invoke [`test-provenance-guard`](../../quality/test-provenance-guard/SKILL.md) on179 any TypeScript helpers the flow imports (e.g. fixture builders) to180 ensure they call production code instead of a private re-implementation.1813. Open `.github/workflows/eas-e2e.yml` (or the EAS Workflow YAML) and182 confirm the `maestro-cloud` job is wired with `retries: 2` and183 `record_screen: false` per184 [`templates/eas-workflow.yaml`](./templates/eas-workflow.yaml).185186If the heal loop fails to converge:187188- Invoke `confidence(analysis)` on the flow failure.189- If confidence is below 90%, escalate to the user with the Maestro190 log, the spec, and the proposed locator changes — do **not** keep191 healing blindly.192193---194195## Decision flow at a glance196197| Signal | Do |198| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- |199| Bug fixable by a unit or component test | Use [`tdd`](../../quality/tdd/SKILL.md), not this skill. |200| Flow is browser-only or pure web | Use [`e2e-testing`](../e2e-testing/SKILL.md), not this skill. |201| 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. |202| Multi-screen native flow, deep links, permissions, or push | Spec-first feature flow (Phase 1). |203| Flaky existing flow | Healer pass only; do not rewrite without spec context. |204| Locator unstable, no stable `testID` | Propose `testID` diff via the `setTestId` helper. |205| Repo missing Maestro CLI or `.maestro/` or `eas.json` E2E profile | Phase 0 halt + ask permission. |206| Heal loop > 3 attempts | Stop, run `confidence(analysis)`, escalate. |207| Flow passes on first run, never seen failing | Verify any imported helpers via `test-provenance-guard` before declaring done. |208| Existing Detox suite is green and stable | Keep it; see [`references/detox-legacy.md`](./references/detox-legacy.md). |209| Detox suite is brittle through RN upgrades | Migrate flow-by-flow to Maestro; do not rewrite the whole suite at once. |210211---212213## Composes with214215- [`e2e-testing`](../e2e-testing/SKILL.md) — owns web E2E and the216 WebView half of hybrid mobile apps. This skill defers to it for217 anything browser-shaped.218- [`tdd`](../../quality/tdd/SKILL.md) — owns the unit and component layers219 (Jest + React Native Testing Library). This skill defers for220 anything below E2E.221- [`test-provenance-guard`](../../quality/test-provenance-guard/SKILL.md) — runs222 on TypeScript helpers imported by flows to catch tests-by-construction.223- [`confidence`](../../quality/confidence/SKILL.md) — gate when the heal loop fails.224- [`holistic-analysis`](../../analysis/holistic-analysis/SKILL.md) — if a flow is225 failing for reasons no flow rewrite can fix, step back instead of226 patching.227- [`ux`](../../design/ux/SKILL.md) — review accessibility labels separately from228 test IDs (the two should never share a value).229230---231232## References233234- [`references/maestro-cli.md`](./references/maestro-cli.md) — CLI surface235 (`launchApp`, `tapOn`, `assertVisible`, `runFlow`, Studio, doctor).236- [`references/maestro-mcp.md`](./references/maestro-mcp.md) — Maestro237 MCP tools for agent-driven flow generation and healing.238- [`references/eas-workflows.md`](./references/eas-workflows.md) —239 `eas.json` E2E profile, `.maestro/` layout, `maestro-cloud` job240 parameters (`build_id`, `flow_path`, `shards`, `retries`,241 `record_screen`, `device_identifier`).242- [`references/detox-legacy.md`](./references/detox-legacy.md) — when243 to keep an existing Detox suite vs. migrate.244- [`references/pyramid-mobile.md`](./references/pyramid-mobile.md) —245 pyramid math for Expo / RN with Jest, React Native Testing Library,246 and Maestro.247248## Templates249250- [`templates/install-plan.md`](./templates/install-plan.md) — Phase 0251 halt message with the exact commands to install Maestro + EAS CLI252 and scaffold `.maestro/`.253- [`templates/flow.yaml`](./templates/flow.yaml) — sample Maestro flow254 with `launchApp`, `tapOn` (`id:` and `text:` forms), `assertVisible`,255 and `runFlow` includes.256- [`templates/eas-workflow.yaml`](./templates/eas-workflow.yaml) — EAS257 Workflow with a build job and a `maestro-cloud` job.258- [`templates/eas-build-profile.json`](./templates/eas-build-profile.json)259 — `eas.json` snippet for an `e2e` profile producing `.apk` and `.app`.260- [`templates/testid-helper.tsx`](./templates/testid-helper.tsx) —261 `setTestId` helper that maps `testID` and `accessibilityLabel`262 correctly per platform without conflating the two.263264---265266## Anti-patterns (one-liner — full list in [`rules/anti-patterns.md`](./rules/anti-patterns.md))267268- Writing E2E for logic a unit or RNTL component test catches.269- Reusing `accessibilityLabel` as the test selector (it is for screen270 readers; doubling its purpose breaks accessibility).271- Patching the flow with absolute coordinates (`tapOn: { point: "50%, 80%" }`)272 instead of proposing a `testID` diff.273- Running the Healer on every save, or on a passing flow.274- Rebuilding the `.app` / `.apk` on every flow run.275- Generating flows against a stub server, not the real app stack.276- Migrating a stable Detox suite all at once instead of flow-by-flow.277278---279280## Definition of done281282- [ ] Phase 0 preflight passed or installs were user-approved.283- [ ] `specs/<flow>.md` exists and the user reviewed it.284- [ ] `.maestro/<flow>.yaml` passes against a real build.285- [ ] Any imported TS helpers pass `test-provenance-guard`.286- [ ] EAS Workflow `maestro-cloud` job runs the flow on Cloud with287 `retries: 2` and `record_screen: false`.288- [ ] If a `testID` was added, it is in the source diff and committed,289 with no `accessibilityLabel` reuse.290291---292> Source: [mthines/agent-skills](https://github.com/mthines/agent-skills) — distributed by [TomeVault](https://tomevault.io).293<!-- tomevault:4.0:skill_md:2026-06-16 -->