| name | mobile-e2e-maestro |
| description | Automatyzacja testów E2E mobile przez Maestro CLI. Nawigacja, tap, scroll, gesty (swipe, long press, pinch), input, asercje, screenshoty na emulatorze iOS/Android. Deep linking, OAuth flow, biometry. Używaj przy 'testuj UI mobilne', 'zrób screenshot apki', 'agent-mobile', 'E2E mobile', weryfikacji checkboxów Weryfikacja: w fazie review. |
Mobile E2E Testing with Maestro
CLI do automatyzacji testów E2E aplikacji mobilnych iOS/Android. Działa na zbudowanej aplikacji w emulatorze/symulatorze (jak agent-browser dla web — testuje to co user faktycznie widzi i klika).
Setup Check
command -v maestro >/dev/null 2>&1 && echo "Installed: $(maestro --version)" || echo "NOT INSTALLED — instalacja: curl -fsSL 'https://get.maestro.mobile.dev' | bash"
Wymagania platformowe:
- Java 17+ wymagane przez CLI —
JAVA_HOME musi wskazywać na instalację Java 17+ (sprawdź: java -version)
- iOS: macOS + Xcode + iOS Simulator (otwarte:
open -a Simulator)
- Android: Android Studio + emulator AVD (uruchomiony:
emulator -avd <name>)
- Aplikacja zbudowana i zainstalowana (przez
eas build --local lub expo run:ios / expo run:android)
Core Workflow
- Launch: uruchom emulator + zainstaluj aplikację (build z dev clienta wystarczy)
- Flow YAML: zapisz scenariusz w pliku
.yaml (folder .maestro/)
- Run:
maestro test .maestro/login-flow.yaml
- Inspect: artefakty domyślnie w
~/.maestro/tests (macOS/Linux) — zmień lokalizacją przez maestro test --test-output-dir=<dir>. takeScreenshot zapisuje tylko zrzut ekranu; wideo powstaje WYŁĄCZNIE przy jawnym startRecording/stopRecording (albo maestro record), nie automatycznie
appId: com.example.myapp
---
- launchApp
- assertVisible: "Zaloguj się"
- tapOn: "Email"
- inputText: "test@example.com"
- tapOn: "Hasło"
- inputText: "haslo123"
- tapOn:
text: "Zaloguj"
enabled: true
- waitForAnimationToEnd
- assertVisible: "Witaj, Test"
- takeScreenshot: login-success
maestro test .maestro/login-flow.yaml
Common Commands (cheat sheet)
- launchApp
- launchApp:
arguments:
isFirstLaunch: true
- stopApp
- pressKey: back
- pressKey: home
- tapOn: "Submit"
- tapOn:
id: "submit-button"
- tapOn:
text: "Submit"
index: 1
- doubleTapOn: "Item"
- longPressOn: "Item"
- inputText: "hello world"
- eraseText
- copyTextFrom: "Item"
- swipe:
direction: UP
duration: 500
- swipe:
from:
id: "card-1"
direction: LEFT
- scroll
- scrollUntilVisible:
element:
text: "Footer"
- assertVisible: "Welcome"
- assertNotVisible: "Loading"
- assertVisible:
id: "user-avatar"
- waitForAnimationToEnd
- waitForAnimationToEnd:
timeout: 5000
- extendedWaitUntil:
visible: "Loaded"
timeout: 10000
- takeScreenshot: name-without-extension
- startRecording: my-flow
- stopRecording
- openLink: "myapp://auth/callback?code=xyz"
- launchApp:
permissions:
all: allow
Maestro Studio (interactive recording)
Od CLI 2.6.0 Studio nie jest już częścią CLI — to osobna aplikacja desktopowa. Pobranie: https://studio.maestro.dev (macOS: MaestroStudio.dmg, Windows: MaestroStudio.exe, Linux: MaestroStudio.AppImage). Otwierasz aplikację, wybierasz workspace i device w GUI — każdy tap/swipe/input generuje live preview YAML, który kopiujesz do flow file.
Analog do Playwright codegen / agent-browser snapshot. Używaj przy projektowaniu nowego flow — szybciej niż pisanie YAML ręcznie.
Legacy: na CLI <= 2.5.x komenda maestro studio (otwierająca localhost:9999) jeszcze działa — w 2.6.0 usunięta z binarki na rzecz osobnej aplikacji.
Deep Linking (OAuth, push notifications)
appId: com.example.myapp
---
- launchApp
- tapOn: "Zaloguj przez Google"
- openLink: "myapp://auth/callback?code=mock-auth-code-123"
- waitForAnimationToEnd
- assertVisible: "Witaj, Test"
To kluczowy use case mobile, którego nie da się przetestować w expo-router-testing (in-process) — Maestro woła OS przez xcrun simctl openurl / adb shell am start, więc deep link idzie przez prawdziwy router OS.
iOS vs Android — różnice
| Aspekt | iOS | Android |
|---|
appId | com.example.myapp (bundle ID z Xcode) | com.example.myapp (package name z app.json android.package) |
| Emulator setup | iOS Simulator (Xcode) | AVD przez Android Studio / emulator CLI |
| System dialogs | permissions: all: allow (default od 2.0) | permissions: all: allow (default od 2.0) |
| Back gesture | pressKey: back no-op (iOS bez Back) | pressKey: back działa |
| Status bar | Maestro ignoruje | Maestro ignoruje |
Pisz flow cross-platform gdy się da. Gdy musisz różnicować, użyj osobnych plików .maestro/ios/ i .maestro/android/.
Cloud vs Local
Local (default):
- Twój sprzęt (macOS dla iOS), emulator/symulator otwarty
- Free, ale wolne (~30s-2min per flow)
- Idealne do pracy lokalnej i przed-commit verification
Maestro Cloud (maestro cloud):
- Build apki + flow uploadowane do chmury
- Equal devices, parallel runs
- Płatne (po free tier)
- Dla CI / cross-device matrix
maestro test .maestro/
maestro cloud --apiKey "$MAESTRO_API_KEY" build/MyApp.app .maestro/
W tym repo default = local. Cloud rozważ dopiero gdy masz CI pipeline.
Integracja z feature-tester-mobile-e2e
feature-tester-mobile-e2e (subagent) wywołuje ten skill w fazie review. Workflow:
- Czyta checklistę zadania (sekcja "Weryfikacja:")
- Każdy checkbox = jeden flow YAML w
.maestro/
- Uruchamia
maestro test .maestro/<flow>.yaml
- Zbiera screenshoty
- Raportuje pass/fail per checkbox z linkami do screenshotów
Naming convention: .maestro/<feature>-<scenario>.yaml, np. .maestro/auth-login-success.yaml, .maestro/auth-login-empty-fields.yaml.
Granica możliwości Maestro — natywne powierzchnie OS (KRYTYCZNE przy planowaniu E2E)
Maestro steruje WYŁĄCZNIE elementami w accessibility tree Twojej aplikacji. Natywnych powierzchni systemu (renderowanych przez OS, nie przez RN) nie dotknie — flow oparty o nie zawiesi się na timeout i [E2E] cicho spadnie do Operatora. To najczęstszy błąd autora scenariusza E2E.
| Natywna powierzchnia OS | Maestro steruje? | Wzorzec zamiast tego |
|---|
| Picker zdjęć / kamera / picker dokumentów | ❌ NIE | Dane, które wpadłyby tą drogą → wstrzyknij przez service_role (runScript: .maestro/inject-*.js, szablon niżej) i asertuj RENDER (siatka/miniatura/teaser) |
Natywny Alert / ActionSheet (confirm, destructive „Usuń") | ❌ NIE | Krok → Operator checklist jako [Manual] |
| Share sheet, picker kontaktów / kalendarza | ❌ NIE | inject danych lub [Manual] |
| Biometria (Face ID / Touch ID enrollment) | ❌ NIE | [Manual] (mock biometrii = poza harnessem) |
Pełnoekranowy viewer obrazów w natywnym <Modal> (swipe między zdjęciami, ✕, delete) | ❌ NIE | OPEN viewera + asercje (licznik, ✕ widoczny) OK; gesty wewnątrz Modala → [Manual] (etap-12b gallery-fullscreen) |
| Permission dialogs (kamera/geo/powiadomienia) | ⚠️ częściowo | launchApp: permissions: all: allow |
| Tap / scroll / swipe / input / deep link w UI apki | ✅ TAK | normalny flow Maestro |
Reguła autora E2E: zanim napiszesz [E2E], prześledź flow krok po kroku — jeśli któryś krok wymaga natywnej powierzchni z kolumny ❌, NIE pisz flow który ją tapuje. Wstrzyknij dane (service_role) i asertuj render, albo przenieś krok do [Manual]. Wzór inject: .maestro/etap-12-inject-message.js — to wzorzec zewnętrzny (projekt gramywpadla; plik nie istnieje w tym szablonie), REST insert kluczem service_role, omija RLS i natywne UI, trigger realtime = realny dowód toru danych. Minimalny szablon inline poniżej.
Skrypt runScript/inject leci w GraalJS Maestro, NIE w Node (regresja etap-12b — zablokowała wszystkie 4 flow):
- Brak dostępu do filesystemu —
http w GraalJS nie czyta plików z dysku. Binarkę (obraz placeholder) przekazuj inline w body jako string/base64, NIGDY jako {filePath: ...}.
- Brak
require, brak modułów Node — tylko czysty REST (http) + inline payload.
- Sekrety (service_role URL/key, ID rekordu itp.) NIGDY jako wartości w bloku
env: pliku flow (plik leży w repo) — przekaż przez maestro test -e KEY="wartość" (np. wczytane z .env.e2e) albo zmienną środowiskową z prefiksem MAESTRO_ (export MAESTRO_SUPABASE_URL=...). Wewnątrz .js skryptu zmienna jest dostępna jako goły identyfikator JS (np. MAESTRO_SUPABASE_URL), w YAML flow wyłącznie jako referencja ${KEY}.
Minimalny szablon inline (.maestro/inject-example.js):
const response = http.post(MAESTRO_SUPABASE_URL + "/rest/v1/messages", {
headers: {
apikey: MAESTRO_SUPABASE_SERVICE_ROLE_KEY,
Authorization: "Bearer " + MAESTRO_SUPABASE_SERVICE_ROLE_KEY,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
conversation_id: MAESTRO_CONVERSATION_ID,
content: "E2E seed message",
}),
});
if (response.status !== 201) {
throw new Error("Inject failed: " + response.status + " " + response.body);
}
Harness E2E w tym repo
Autonomiczne E2E (uruchamiane przez feature-tester-mobile-e2e / autopilot) leci na dedykowanym projekcie Supabase skonfigurowanym w .env.e2e — NIGDY na środowisku dev/prod:
- Konto testowe:
E2E_TEST_EMAIL / E2E_TEST_PASSWORD — flow loguje się na stałe konto zamiast tworzyć nowego usera za każdym razem.
- Seedy:
.maestro/<flow>-seed.sql — idempotentne (upsert, nie insert), referencja do konta testowego przez email, nie stały ID (ID różni się między środowiskami).
- Sekrety: wyłącznie z
.env.e2e, przekazywane przez -e/MAESTRO_* (patrz sekcja wyżej) — nigdy hardcoded w YAML/JS.
Pełne szczegóły konfiguracji i autorstwo deliverables (kto pisze seed, kto flow): dev-plan §3.4b oraz .claude/templates/e2e-env/README.md.
Common gotchas
- Tekst polski: Maestro obsługuje UTF-8, nie escape'uj diakrytyków.
tapOn: "Zaloguj się" działa.
- Animacje: zawsze
waitForAnimationToEnd po nawigacji, w przeciwnym razie tap na nieobecny element zwraca timeout.
- Toast / alerty: zniknią same — łap je przez
extendedWaitUntil: visible: "..." timeout: 3000.
testID: w RN dodawaj testID="submit-button" na komponentach żeby id: matchowało. Inaczej Maestro polega na tekście, co łamie się przy tłumaczeniach.
- Hot reload: Maestro NIE działa na devbuild z hot reloadu — buduj release/preview build (
eas build --profile preview).
Quick reference
maestro --version
maestro test .maestro/login.yaml
maestro test .maestro/
maestro hierarchy
maestro list-devices
maestro start-device --platform ios