Maestro E2E
Status: Active
Industry-standard React Native end-to-end testing via Maestro (Detox is maintenance-mode as of 2024). Round 28 production-readiness.
What's covered
4 CI-enabled smoke flows in apps/mobile/.maestro/flows/:
| # | Flow | What it verifies |
|---|---|---|
| 01 | login | Dev account login → home tab loads |
| 02 | main-tabs-smoke | Login → deterministic post-login tab surfaces |
| 08 | club-growth-media | Seeded club discovery/profile/media/admin surfaces |
| 09 | club-join-funnels | R1-2 invite, open, and approval join funnels |
Targeted R1 acceptance proof:
| # | Flow | What it verifies |
|---|---|---|
| packet | r1-evidence-rerun | Sequential hosted R1 packet: 01/02/08/09, then 10, then generated share deep links |
| 10 | club-create-first-session-cta | R1-2 zero-club create-club -> first-session action proof |
| generated | r1-share-deep-links | R1-3 generated custom-scheme /s, /m, /p, /lb, and /join-club?code=... receiver proof |
Deferred until deterministic seeds exist:
| # | Flow | Required seed |
|---|---|---|
| 03 | rsvp-toggle | Session detail state with coachmarks suppressed |
| 04 | score-submit | Match-board session ready for score entry |
| 05 | disputed-alert | Disputed match attached to the dev account |
| 06 | share-session | Shareable session detail state |
The deferred flows still chain via runFlow: so they can be re-enabled once seed automation is reliable.
Running locally
Prereq: Maestro CLI installed.
# macOS / Linux
curl -Ls "https://get.maestro.mobile.dev" | bash
# Verify
maestro --versionRun all flows against a connected emulator or device:
yarn e2e:localRun a single flow:
yarn e2e:flow apps/mobile/.maestro/flows/03-rsvp-toggle.yamlRun the targeted club growth/media smoke after installing a preview build on a connected device or emulator:
yarn e2e:club-growth-mediaThat command seeds club_growth first, then runs 08-club-growth-media.yaml. The flow captures screenshots for My Clubs, Discover, public profile, inline application, and settings-gallery states.
Run the R1-2 club join-funnel smoke after installing a preview build on a connected device or emulator:
yarn e2e:club-join-funnelsThat command seeds club_growth first, then runs 09-club-join-funnels.yaml. The flow verifies invite-code join, open-club direct join, and approval-request submission against repeatable seeded fixtures.
Prepare and run the R1-3 custom-scheme share receiver smoke after installing a preview build on a connected device or emulator:
yarn prepare:r1-share-deep-link-flow
maestro test apps/mobile/.maestro/generated/r1-share-deep-links.yamlprepare:r1-share-deep-link-flow seeds club_growth through the deployed edge function and writes ignored generated YAML with the current shareProfileId, shareSessionId, shareClubId, shareMatchId, and shareInviteCode. The generated flow logs in, then opens twomore://p/<id>, twomore://s/<id>, twomore://lb/<id>, twomore://m/<id>, and twomore://join-club?code=<inviteCode> and screenshots each landing surface. The invite-code step submits without typing, proving the linked route populated the join form before the RPC join.
Running in CI
CI integration lives at apps/mobile/.eas/workflows/maestro-e2e.yml. EAS Workflows builds a preview Android APK, seeds club_growth with scripts/run-scenario.mjs in a before_maestro_tests hook, then runs the enabled flows against the build on Expo's hosted emulator fleet — no BrowserStack / Sauce Labs accounts needed. The build-reuse workflow runs the same enabled flow list against an existing preview build.
Trigger:
# Full run (builds a fresh APK, ~28 min, then ~5 min of flows)
cd apps/mobile && eas workflow:run .eas/workflows/maestro-e2e.yml
# Rerun against an EXISTING build (test-YAML-only changes — skips the rebuild)
cd apps/mobile && eas workflow:run .eas/workflows/maestro-e2e-rerun.yml -F build_id=<id>
# <id> from the prior run's build_android job, or: eas build:list --platform android --limit 3
# R1-3 generated custom-scheme receiver proof against an EXISTING build
cd apps/mobile && eas workflow:run .eas/workflows/maestro-r1-share-deep-links-rerun.yml -F build_id=<id>
# R1 final hosted evidence packet against an EXISTING build
cd apps/mobile && eas workflow:run .eas/workflows/maestro-r1-evidence-rerun.yml -F build_id=<id>
# Automatic — fires through the scheduled/manual GitHub workflow.Pick the cheapest sufficient run (owner directive 2026-06-12): if the only change since the last green build is flow YAML / seed logic, the app binary is identical — use maestro-e2e-rerun.yml with the previous build_id instead of paying the rebuild. Keep flow_path in the two files in sync. Hosted runs are release gates, not an iteration loop — iterate on selectors against seeded local/preview state first.
Use maestro-r1-evidence-rerun.yml only when the candidate source state is ready for an R1 evidence packet. It reuses one Android preview build and runs three sequential jobs:
- seeded club-growth hosted suite (
01,02,08,09); - seeded zero-club create-club first-session proof (
10); - generated R1 share deep-link proof (
twomore://p,/s,/lb,/m, and/join-club?code=...).
yarn check:r1-evidence-workflow keeps the packet workflow, the full build workflow, the build-reuse workflow, and the two targeted R1 rerun workflows in sync so new agents do not have to compare YAML by hand.
Free-tier budget: ~30 min/month. Keep scheduled runs sparse and use manual dispatch before risky merges.
Operational gotchas (observed 2026-06-11/12):
eas workflow:runcan hang silently during project upload (a backgrounded CLI stalled overnight without creating a run, twice across sessions). Trigger in the foreground with a timeout and confirm a run ID came back before polling.- A run that IS created shows
build_android: IN_PROGRESSwithin ~1 min; check the underlying build witheas build:view <build_id>to distinguish "building" from "stuck in queue".
Current R1 hosted-smoke evidence
Latest R1 hosted evidence as of 2026-06-28:
| Run ID | Source / artifact | Result |
|---|---|---|
019f0ae7-a9e7-722c-a9f6-18855f67ef1e | Reused build 6aa365f6-7814-4250-a708-a27843a919dc | FAILURE; 01/02 passed, 08/09 failed on stale flow assertions |
019f0af0-c231-7cce-a1e7-0708fa85d4bb | Reused build 6aa365f6-7814-4250-a708-a27843a919dc | FAILURE; 01/02/08 passed, 09 failed after open join waiting for member-detail "소식" |
019f0b09-d61c-70aa-8dfe-4b28b1cbf87d | Preview OTA 1ac260bd on reused build 6aa365f6 | FAILURE; 01/02/08 passed, 09 advanced past invite/open joins and failed at club.public-profile.application-message |
019f0b2a-b6db-7f2d-b73d-e95cc0d9456a | Preview OTA 28950118 on reused build 6aa365f6 | FAILURE; 01/02/09 passed, 08 failed on stale 가입 신청하기 assertion after approval submit moved to 신청 보내기 |
019f0b37-4a40-7eff-8727-d31d22a43a43 | Preview OTA 28950118; flow commit 7c0bd427 | SUCCESS; enabled 01/02/08/09 hosted suite passed |
019f0bb1-2f5f-7845-bc8b-3d8ab65bbdfe | Preview OTA ff478b2e; source commit a5d2a913 | SUCCESS; targeted 10 flow passed after seeding r1_create_club_zero; JUnit 019f0bb5-212d, results 019f0bb5-1f40 |
019f0c17-0e66-7ad8-b0b6-47c2eb9747ef | Reused build 6aa365f6; pre-fix preview OTA | FAILURE; generated R1-3 share flow opened /s but mobile receiver showed 이 일정을 찾을 수 없어요 under member-only RLS |
019f0c2b-9027-7d85-b3c4-a66dc39e99ef | Preview OTA e01fc19c; source commit 111dbf77 | FAILURE; /s fallback was fixed, /lb landed on club profile, but generated flow asserted below-the-fold 소식 |
019f0c32-1afc-70c5-82d8-185df88e8ad4 | Preview OTA e01fc19c; generator commit 55a90126 | SUCCESS; generated R1-3 custom-scheme flow passed for /p, /s, /lb, and /m; JUnit 019f0c35-bbfc, results 019f0c35-ba30 |
019f0c63-85bc-7f19-9d9b-d449ed548eb1 | Existing build 6aa365f6; generator commit c9d81257 | SUCCESS; generated R1-3 custom-scheme flow passed invite-code /join-club?code=JOINR12 prefill and submit; JUnit 019f0c67-620e, results 019f0c67-6014 |
R1-2 is accepted from the combined evidence: backend join RPC proof, hosted 01/02/08/09 join-funnel proof, preview OTA ff478b2e, and hosted 10 create-club -> first-session proof. R1-3 now has hosted Android custom-scheme recipient-return proof from generated seeded profile, session, leaderboard, match, and invite-code links. R1 remains active because R1-3 still needs Kakao-native share and credentialed true HTTP app-link preview-build proof, and R1-4 still needs a green consolidated evidence-packet run or an explicitly accepted set of equivalent hosted run IDs. Public web receiver smoke, mobile receiver fallback, preview OTA, static app-link config, custom-scheme hosted proof, and the consolidated packet workflow are tracked in the R1 source pack and release evidence ledger.
testID conventions
Maestro matches by text first, accessibility ID second. The flow files prefer text-matching for resilience (a translated label still works); testIDs are documented here for cases where text is ambiguous.
| Surface | testID |
|---|---|
| Main tab roots | tab.home, tab.activity, tab.clubs, tab.records, tab.profile |
| Home tab bell icon | home.bell-icon |
| Home/session cards | session-card |
| Session detail share button | share-session |
| Match-board score steppers | (uses text: '+' with positional index) |
| Dev-panel disputed-match scenario | dev-panel.scenario.disputed-match |
| Club growth cards/profile/settings | club.my.card, club.discover.card, club.public-profile.*, club.settings.gallery* |
| Club detail first-session shortcut | club.detail.create-session-shortcut |
Adding new testIDs: prefix with <surface>.<element> (e.g., session-detail.confirm-rsvp). Match the convention from @twomore/ui's Pressable primitive — it forwards testID through to the underlying RN view.
Debugging failing flows
- Run with --debug-output:
maestro test apps/mobile/.maestro/flows/01-login.yaml --debug-output=./debugproduces a screenshot + view-hierarchy dump on failure. - Use maestro studio:
maestro studioopens an interactive inspector showing what Maestro sees on screen (useful when anassertVisiblefails because the text is wrapped or hidden behind a modal). - Add
optional: trueto brittle assertions to gather more signal — Maestro continues past the missing element instead of short-circuiting the whole flow. - Slow CI down with
delay:when an animation is causing race conditions (better thantimeout:bumps which mask the real problem).
Out of scope (deferred to a later round)
- iOS in EAS Workflows: Currently the workflow only runs Android. iOS requires Apple Developer account credentials + a TestFlight-eligible build. Add once iOS preview build is set up (parked on credentials).
- Visual diff testing: Maestro supports screenshot diffs but our Tamagui theming + safe-area variations make per-pixel matching fragile. Defer until the surface stabilizes.
- Remaining seeded action flows: flows 03–06 still need dedicated deterministic seed hooks or direct scenario setup before they are safe for hosted CI.
- Performance regression detection: Maestro can measure frame times via
assertNoFlakiness. Couple with Sentry profiling (Round 26) for a complete perf signal — TBD.