Skip to content

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/:

#FlowWhat it verifies
01loginDev account login → home tab loads
02main-tabs-smokeLogin → deterministic post-login tab surfaces
08club-growth-mediaSeeded club discovery/profile/media/admin surfaces
09club-join-funnelsR1-2 invite, open, and approval join funnels

Targeted R1 acceptance proof:

#FlowWhat it verifies
packetr1-evidence-rerunSequential hosted R1 packet: 01/02/08/09, then 10, then generated share deep links
10club-create-first-session-ctaR1-2 zero-club create-club -> first-session action proof
generatedr1-share-deep-linksR1-3 generated custom-scheme /s, /m, /p, /lb, and /join-club?code=... receiver proof

Deferred until deterministic seeds exist:

#FlowRequired seed
03rsvp-toggleSession detail state with coachmarks suppressed
04score-submitMatch-board session ready for score entry
05disputed-alertDisputed match attached to the dev account
06share-sessionShareable 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.

bash
# macOS / Linux
curl -Ls "https://get.maestro.mobile.dev" | bash

# Verify
maestro --version

Run all flows against a connected emulator or device:

bash
yarn e2e:local

Run a single flow:

bash
yarn e2e:flow apps/mobile/.maestro/flows/03-rsvp-toggle.yaml

Run the targeted club growth/media smoke after installing a preview build on a connected device or emulator:

bash
yarn e2e:club-growth-media

That 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:

bash
yarn e2e:club-join-funnels

That 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:

bash
yarn prepare:r1-share-deep-link-flow
maestro test apps/mobile/.maestro/generated/r1-share-deep-links.yaml

prepare: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:

bash
# 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:

  1. seeded club-growth hosted suite (01, 02, 08, 09);
  2. seeded zero-club create-club first-session proof (10);
  3. 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:run can 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_PROGRESS within ~1 min; check the underlying build with eas 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 IDSource / artifactResult
019f0ae7-a9e7-722c-a9f6-18855f67ef1eReused build 6aa365f6-7814-4250-a708-a27843a919dcFAILURE; 01/02 passed, 08/09 failed on stale flow assertions
019f0af0-c231-7cce-a1e7-0708fa85d4bbReused build 6aa365f6-7814-4250-a708-a27843a919dcFAILURE; 01/02/08 passed, 09 failed after open join waiting for member-detail "소식"
019f0b09-d61c-70aa-8dfe-4b28b1cbf87dPreview OTA 1ac260bd on reused build 6aa365f6FAILURE; 01/02/08 passed, 09 advanced past invite/open joins and failed at club.public-profile.application-message
019f0b2a-b6db-7f2d-b73d-e95cc0d9456aPreview OTA 28950118 on reused build 6aa365f6FAILURE; 01/02/09 passed, 08 failed on stale 가입 신청하기 assertion after approval submit moved to 신청 보내기
019f0b37-4a40-7eff-8727-d31d22a43a43Preview OTA 28950118; flow commit 7c0bd427SUCCESS; enabled 01/02/08/09 hosted suite passed
019f0bb1-2f5f-7845-bc8b-3d8ab65bbdfePreview OTA ff478b2e; source commit a5d2a913SUCCESS; targeted 10 flow passed after seeding r1_create_club_zero; JUnit 019f0bb5-212d, results 019f0bb5-1f40
019f0c17-0e66-7ad8-b0b6-47c2eb9747efReused build 6aa365f6; pre-fix preview OTAFAILURE; generated R1-3 share flow opened /s but mobile receiver showed 이 일정을 찾을 수 없어요 under member-only RLS
019f0c2b-9027-7d85-b3c4-a66dc39e99efPreview OTA e01fc19c; source commit 111dbf77FAILURE; /s fallback was fixed, /lb landed on club profile, but generated flow asserted below-the-fold 소식
019f0c32-1afc-70c5-82d8-185df88e8ad4Preview OTA e01fc19c; generator commit 55a90126SUCCESS; generated R1-3 custom-scheme flow passed for /p, /s, /lb, and /m; JUnit 019f0c35-bbfc, results 019f0c35-ba30
019f0c63-85bc-7f19-9d9b-d449ed548eb1Existing build 6aa365f6; generator commit c9d81257SUCCESS; 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.

SurfacetestID
Main tab rootstab.home, tab.activity, tab.clubs, tab.records, tab.profile
Home tab bell iconhome.bell-icon
Home/session cardssession-card
Session detail share buttonshare-session
Match-board score steppers(uses text: '+' with positional index)
Dev-panel disputed-match scenariodev-panel.scenario.disputed-match
Club growth cards/profile/settingsclub.my.card, club.discover.card, club.public-profile.*, club.settings.gallery*
Club detail first-session shortcutclub.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

  1. Run with --debug-output: maestro test apps/mobile/.maestro/flows/01-login.yaml --debug-output=./debug produces a screenshot + view-hierarchy dump on failure.
  2. Use maestro studio: maestro studio opens an interactive inspector showing what Maestro sees on screen (useful when an assertVisible fails because the text is wrapped or hidden behind a modal).
  3. Add optional: true to brittle assertions to gather more signal — Maestro continues past the missing element instead of short-circuiting the whole flow.
  4. Slow CI down with delay: when an animation is causing race conditions (better than timeout: 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.

Markdown remains the source of truth. Run yarn docs:check before handoff.