| 1 | # Mobile E2E (Maestro on a physical Android device)
|
| 2 |
|
| 3 | Smoke coverage for the core journey: **login → register a mood → logout**. Runs
|
| 4 | on a real Android device over `adb` (no emulator), against a dev build of the
|
| 5 | app talking to the normal local API
|
| 6 |
|
| 7 | ## What you need
|
| 8 |
|
| 9 | 1. **API up, with the seeded user.**: the app points at whichever API
|
| 10 | `EXPO_PUBLIC_API_BASE_URL` names
|
| 11 |
|
| 12 | ```sh
|
| 13 | docker compose up -d db valkey api
|
| 14 | cd api && npm run seed # idempotent; creates e2e@example.com / e2e-password
|
| 15 | ```
|
| 16 |
|
| 17 | 2. **Metro serving the dev build**, with the device-reachable API URL baked in
|
| 18 | (`EXPO_PUBLIC_*` is inlined at bundle time, so Metro must be started *after*
|
| 19 | `frontend/.env.local` is set: see `npm run test:e2e:env`)
|
| 20 |
|
| 21 | 3. **The dev build installed** (`com.eletrotupi.orbit`) and the device visible in
|
| 22 | `adb devices`
|
| 23 |
|
| 24 | 4. **Maestro** on `PATH` (`https://get.maestro.mobile.dev`)
|
| 25 |
|
| 26 | ## Running
|
| 27 |
|
| 28 | ```sh
|
| 29 | cd frontend
|
| 30 | npm run test:e2e # adb prep + the tagged smoke flows
|
| 31 | npm run test:e2e:env # print the EXPO_PUBLIC_* values Metro needs
|
| 32 | ```
|
| 33 |
|
| 34 | `scripts/e2e.sh` is POSIX `sh`, so it runs unchanged on Debian/Ubuntu and on
|
| 35 | Alpine (busybox ash). It turns the device's animation scales off for the run and
|
| 36 | restores the previous values on exit — that setting is persisted on the device,
|
| 37 | so it is never left behind.
|
| 38 |
|
| 39 | A single flow, while iterating (a few minutes faster than the whole suite):
|
| 40 |
|
| 41 | ```sh
|
| 42 | cd frontend
|
| 43 | npm run test:e2e:env # prints HOST_IP, the one machine-specific value
|
| 44 |
|
| 45 | maestro test e2e/mood.yaml \
|
| 46 | --env HOST_IP=<lan-ip> --env PORT=8081 \
|
| 47 | --env E2E_EMAIL=e2e@example.com --env E2E_PASSWORD=e2e-password
|
| 48 | ```
|
| 49 |
|
| 50 | Three flows take roughly 8 minutes: every flow clears app state, so each one
|
| 51 | pays for a cold bundle load and the dev-client startup dance
|
| 52 |
|
| 53 | ## How the flows are structured
|
| 54 |
|
| 55 | Every flow is **self-contained and order-independent**: Maestro runs them in an
|
| 56 | arbitrary order, so none may assume another ran first. Each starts with:
|
| 57 |
|
| 58 | ```yaml
|
| 59 | - runFlow:
|
| 60 | file: helpers/_login.yaml
|
| 61 | ```
|
| 62 |
|
| 63 | and `helpers/_login.yaml` does the whole reset-and-authenticate: `clearState`
|
| 64 | => deep-link the dev build => log in => dismiss the notification prompt. That
|
| 65 | means `clearState` is what guarantees a logged-out start, which has three
|
| 66 | consequences worth knowing:
|
| 67 |
|
| 68 | - it resets **runtime permissions**, so the notification dialog shows up again
|
| 69 | and must be allowed (see copy matches below);
|
| 70 | - it resets **expo-dev-client**, so its first-run overlays appear ("Continue",
|
| 71 | then the dev-menu panel's "Close");
|
| 72 | - it discards the downloaded JS bundle, hence the cold start
|
| 73 |
|
| 74 | The app is intentionally **not** launched with `exp://`: Expo Go owns that
|
| 75 | scheme, and driving Expo Go while asserting on `com.eletrotupi.orbit` will fail
|
| 76 | with "element not found" everywhere. The dev build owns
|
| 77 | `orbit://expo-development-client/?url=...`, kinda hacky but I don't care
|
| 78 |
|
| 79 | ## testID convention
|
| 80 |
|
| 81 | `id`s are English even though the UI is pt-BR. Flows only touch screens that
|
| 82 | pass an explicit `testID`
|
| 83 |
|
| 84 | Auto-derived fallbacks exist when a screen passes nothing: `Button` →
|
| 85 | `button-<slug(title)>`, `Input`/`TextArea` → `input-<slug(label)>`,
|
| 86 | `MoodSelector` → `mood-<id>`. The slug util lives in `Button.tsx` / `Input.tsx`
|
| 87 | (NFD strip, lowercase, kebab-style)
|
| 88 |
|
| 89 | Every flow file: **including subflows** — must declare `appId:
|
| 90 | com.eletrotupi.orbit` in its config section. Maestro 2.x requires it per file;
|
| 91 | the workspace `.maestro/config.yaml` cannot supply it
|
| 92 |
|
| 93 | ## The unavoidable copy matches
|
| 94 |
|
| 95 | A handful of strings cannot be reached by `testID` because they belong to the
|
| 96 | OS or to dev tooling. These are deliberate, and the only ones a flow may assert
|
| 97 | on:
|
| 98 |
|
| 99 | | Where | Selector | Why |
|
| 100 | | --- | --- | --- |
|
| 101 | | History card annotation | `"e2e smoke register"` | the one *data* assertion; proves write-through |
|
| 102 | | Notification permission | `com.android.permissioncontroller:id/permission_allow_button` | AOSP system dialog |
|
| 103 | | expo-dev-client onboarding | `^Continue$` | dev tooling, not app UI |
|
| 104 | | expo-dev-client dev menu | `^Close$` (content-desc) | dev tooling, not app UI |
|
| 105 |
|
| 106 | Everything else is greeted via `testID`
|
| 107 |
|
| 108 | ## Known gotchas
|
| 109 |
|
| 110 | - **The formSheet opens at its half detent.** `entry/mood-components` uses
|
| 111 | `sheetInitialDetentIndex: 1` of `[0.25, 0.5, 1]`, so only the first rows of
|
| 112 | the component list are mounted
|
| 113 | - The soft keyboard covers `button-login`; the flow calls `hideKeyboard` first
|