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
config.yaml's enabled flows: list in apps/mobile/.maestro/:
| # | Flow | What it verifies | Seed |
|---|---|---|---|
| 01 | login | Dev account login → home tab loads | none |
| 02 | main-tabs-smoke | Login → deterministic post-login tab surfaces | none |
| 03 | rsvp-toggle | Session detail state + RSVP confirm | home_rsvp_pending (yarn e2e:rsvp) |
| 04 | score-submit | Match-board score entry → 미확정 status | home_score_verification (yarn e2e:score-submit) |
| 05 | disputed-alert | HomeDisputedAlertBanner on a disputed match | live_showcase (yarn e2e:disputed-alert) |
| 06 | share-session | Session-detail share sheet → twomore.app URL | home_upcoming (yarn e2e:share-session) |
| 08 | club-growth-media | Seeded club discovery/profile/media/admin surfaces | club_growth (yarn e2e:club-growth-media) |
| 09 | club-join-funnels | R1-2 invite, open, and approval join funnels | club_growth (yarn e2e:club-join-funnels) |
| 10 | club-create-first-session-cta | R1-2 zero-club create-club -> first-session proof | r1_create_club_zero (yarn e2e:club-create-first-session-cta) |
| 11 | venue-finder | Nav-chip counts, 연습장/샵/테니스장 views, region filter, first-result detail | none — nationwide venue corpus is live data (yarn e2e:venue-finder) |
| 12 | venue-detail-edit | Correction-modal name/address fields; shop kind-gating (no 코트 수) | none — nationwide venue corpus is live data (yarn e2e:venue-detail-edit) |
03–06 previously read "assumes a pre-seeded session"; each now has a deterministic run-scenario.mjs seed wired through its yarn e2e:* script (see each flow's header comment for the exact seed key). Known gap (found while wiring 11/12): flow 03's session-card selector matches the literal substring 복식 경쟁, but the home_rsvp_pending/home_score_verification seed scenarios title their sessions 역삼 복식 정기전 (competitive doubles, not literally 경쟁) — the selector likely needs updating to match the real seeded title before yarn e2e:rsvp / yarn e2e:score-submit pass against a connected device. Not fixed in this pass (out of scope); flagged for whoever next runs these against an emulator.
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 |
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.
Run the venue-finder smoke after installing a preview build on a connected device or emulator:
yarn e2e:venue-finder
yarn e2e:venue-detail-editNeither seeds anything first — the nationwide venue directory is live production data (public.venues), not a seeded scenario. 11-venue-finder.yaml covers nav-chip counts, the 연습장/샵/테니스장 views, the region filter narrowing the browse scope, and the first result's detail screen. 12-venue-detail-edit.yaml covers the correction-modal name/address fields and the per-kind field gating (a 샵 venue never offers 코트 수 — SHOP_EXCLUDED_FIELDS, migration 00573).
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 |
| Venue finder nav chips | venue.nav.all, venue.nav.court, venue.nav.practice, venue.nav.shop, venue.nav.saved |
| Venue finder filter toggle | venue.filter.toggle |
| Venue directory result card | venue.card (every card carries the same id — use index: to pick one) |
| Venue detail kind badge / edit / address | venue.detail.kind, venue.detail.edit.name, venue.detail.address |
| Venue correction modal fields/submit | venue.correct.<field> (e.g. venue.correct.name, venue.correct.court_count), venue.correct.submit |
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.
- 03's stale session-title selector: flows 03–06 now have deterministic seeds (see "What's covered" above), but 03's session-card text selector (
복식 경쟁) doesn't match the actual seeded title (역삼 복식 정기전) — needs a selector fix before these 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.