diff --git a/.github/workflows/maestro-cloud.yml b/.github/workflows/maestro-cloud.yml new file mode 100644 index 0000000..1a248c0 --- /dev/null +++ b/.github/workflows/maestro-cloud.yml @@ -0,0 +1,94 @@ +# Maestro Cloud — E2E template for rebreak-native. +# STATUS: TEMPLATE ONLY — not active. Requires User confirmation before enabling. +# +# Trigger: manual dispatch OR PR to main (commented out — enable after User GO). +# Requires: +# - MAESTRO_CLOUD_API_KEY in GitHub Actions secrets +# - EAS_TOKEN in GitHub Actions secrets +# - E2E_TEST_USER + E2E_TEST_PASSWORD in GitHub Actions secrets +# - Maestro Cloud account configured at mobile.dev + +name: Maestro Cloud E2E (rebreak-native) + +on: + workflow_dispatch: + inputs: + platform: + description: "Target platform" + required: true + default: "ios" + type: choice + options: + - ios + - android + # Uncomment to run on PRs — only after User approval: + # pull_request: + # branches: [main] + # paths: + # - "apps/rebreak-native/**" + # - "apps/rebreak-native/.maestro/**" + +jobs: + maestro-cloud: + name: E2E (${{ inputs.platform || 'ios' }}) + runs-on: ubuntu-latest + + # Skip entirely if Maestro Cloud key is not configured — + # avoids CI failure on forks or before Cloud is set up. + if: ${{ secrets.MAESTRO_CLOUD_API_KEY != '' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: apps/rebreak-native + + # Build app via EAS — requires EAS_TOKEN secret and eas.json configured. + # Profile "preview" must produce a .ipa (iOS) or .apk (Android). + - name: Build with EAS + uses: expo/expo-github-action@v8 + with: + eas-version: latest + token: ${{ secrets.EAS_TOKEN }} + + - name: EAS Build + run: | + eas build \ + --platform ${{ inputs.platform || 'ios' }} \ + --profile preview \ + --non-interactive \ + --output ./build-artifact + working-directory: apps/rebreak-native + + # Install Maestro CLI + - name: Install Maestro CLI + run: curl -Ls "https://get.maestro.mobile.dev" | bash + env: + MAESTRO_VERSION: 1.39.0 + + - name: Add Maestro to PATH + run: echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + # Upload build + run flows on Maestro Cloud + - name: Run Maestro Cloud + run: | + maestro cloud \ + --apiKey ${{ secrets.MAESTRO_CLOUD_API_KEY }} \ + --app ./build-artifact \ + --device ${{ inputs.platform || 'ios' }} \ + --env=E2E_TEST_USER=${{ secrets.E2E_TEST_USER }} \ + --env=E2E_TEST_PASSWORD=${{ secrets.E2E_TEST_PASSWORD }} \ + apps/rebreak-native/.maestro/ + working-directory: ${{ github.workspace }} diff --git a/apps/rebreak-native/.maestro/SETUP.md b/apps/rebreak-native/.maestro/SETUP.md index 0eb17bc..c332e8a 100644 --- a/apps/rebreak-native/.maestro/SETUP.md +++ b/apps/rebreak-native/.maestro/SETUP.md @@ -74,27 +74,42 @@ Flow-Headern als `appId: org.rebreak.app`. Flows benoetigen Test-User-Credentials. **Nie** hardcoden — immer als Env-Vars uebergeben. -```bash -export E2E_TEST_USER=claude-android-test -export E2E_TEST_PASSWORD= -``` - -Oder via Infisical: +### Option A: direktes `--env` Flag ```bash -infisical run -- maestro test apps/rebreak-native/.maestro/auth/signin.yaml +maestro test \ + --env=E2E_TEST_USER=admin \ + --env=E2E_TEST_PASSWORD= \ + apps/rebreak-native/.maestro/auth/email-signin.yaml ``` +### Option B: Infisical Wrapper + +```bash +infisical run -- maestro test apps/rebreak-native/.maestro/auth/email-signin.yaml +``` + +Voraussetzung: Infisical-Projekt hat `E2E_TEST_USER` und `E2E_TEST_PASSWORD` als Secrets. + Variablen die Flows erwarten: -| Var | Beschreibung | -|----------------------|-----------------------------------------------| -| `E2E_TEST_USER` | Username ohne @rebreak.internal | -| `E2E_TEST_PASSWORD` | Passwort des Test-Users auf Staging | +| Var | Beschreibung | +|----------------------|-------------------------------------------------------| +| `E2E_TEST_USER` | Username-Teil der E-Mail (ohne @rebreak.internal) | +| `E2E_TEST_PASSWORD` | Passwort des Test-Users auf Staging | Wichtig: Der Backend-Server haengt `@rebreak.internal` automatisch an den Username. In den Flows steht deshalb `${E2E_TEST_USER}@rebreak.internal` als E-Mail-Input. +### Test-Account (aktueller Stand) + +- **`admin@rebreak.org`** — email-basierter Account, Passwort in Infisical als `E2E_TEST_PASSWORD` + Dann: `E2E_TEST_USER=admin` +- **`charioanouar@gmail.com`** — Google OAuth only, kein Passwort → kann NICHT fuer Maestro-Email-Login genutzt werden +- **`claude-android-test@rebreak.internal`** — dedizierter CI-Test-Account (Erstellung via Service-Role noetig wenn nicht vorhanden) + +Empfehlung: `admin`-Account fuer lokale Flow-Tests nutzen. + --- ## 4. Flows ausfuehren @@ -195,12 +210,25 @@ Test-User muss **vorab** auf dem Staging-Backend existieren: ## 8. Flow-Uebersicht -| Flow | Was wird geprueft | -|-----------------------------------|----------------------------------------------------------| -| `auth/signin.yaml` | App startet, Login funktioniert, Home-Feed sichtbar | -| `urge/start-session.yaml` | SOS-Button im Dropdown erreichbar, Lyra-Screen laedt | -| `community/post.yaml` | ComposeCard oeffnet, Text-Input funktioniert, Post sendet| -| `profile/view-profile.yaml` | Profil-Navigation via Dropdown, ProfileScreen laedt | +| Flow | Was wird geprueft | Stabil? | +|---|---|---| +| `auth/signin.yaml` | App startet, Login via Email+Pw, Home-Feed sichtbar | Ja (text-selektoren) | +| `auth/email-signin.yaml` | Identisch — aktuelle Version mit besseren Kommentaren | Ja | +| `urge/start-session.yaml` | SOS im Dropdown erreichbar, Lyra-Screen laedt | Koordinaten-Fallback | +| `urge/sos-flow.yaml` | SOS → Lyra-Chat → "Atemübung" Chip → BreathingDrawer | LLM-abhaengig | +| `community/post.yaml` | ComposeCard, Text-Input, Submit | Ja | +| `community/create-post.yaml` | Identisch — aktuelle Version | Ja | +| `profile/view-profile.yaml` | Profil-Navigation, ProfileHeader, StatsBar | Koordinaten-Fallback | +| `profile/view-and-edit.yaml` | Profil → Edit → Nickname aendern → Speichern | Koordinaten-Fallback | +| `profile/demographics.yaml` | DemographicsAccordion toggle, WheelPicker oeffnet | Text-selektoren | +| `settings/dark-theme.yaml` | Settings → Theme → Dunkel | Native-Menu-Limitation | + +**Koordinaten-Fallback** = Flow nutzt `point: "x%, y%"` fuer Avatar-Button, weil kein `testID` vorhanden. +Bricht wenn Header-Layout geaendert wird. Betroffene testIDs: `TODO_TESTIDS.md`. + +**Native-Menu-Limitation** = `@react-native-menu/menu` (UIMenu auf iOS) kann Maestro moeglicherweise +nicht interagieren — Flow koennte an diesem Step haengen. Wenn `settings/dark-theme.yaml` immer +an "Systemstandard" haengt: bekanntes Problem, kein Maestro-Bug, sondern iOS-Restriktion. --- diff --git a/apps/rebreak-native/.maestro/TODO_TESTIDS.md b/apps/rebreak-native/.maestro/TODO_TESTIDS.md new file mode 100644 index 0000000..b64cf3c --- /dev/null +++ b/apps/rebreak-native/.maestro/TODO_TESTIDS.md @@ -0,0 +1,104 @@ +# TODO: testID additions needed for stable Maestro selectors + +These are components that need `testID="..."` added by the UI agent (rebreak-native-ui domain). +Ahmed does NOT add these — list is for coordination. + +Priority: HIGH = flow currently uses coordinate fallback or is skipped. +Priority: MEDIUM = flow works via text selector but will break on i18n locale change. +Priority: LOW = nice to have, flow is stable enough without it. + +--- + +## HIGH — Coordinate fallbacks (breaks on layout change) + +| Component | File | Recommended testID | Used by flow | +|---|---|---|---| +| Avatar Pressable (menu trigger) | `components/AppHeader.tsx` line ~109 | `header-avatar-btn` | all flows that open dropdown | +| Nickname edit Pressable in ProfileHeader | `components/profile/ProfileHeader.tsx` | `profile-edit-nickname-btn` | `profile/view-and-edit.yaml` | +| Photo/avatar area tap in ProfileHeader | `components/profile/ProfileHeader.tsx` | `profile-edit-avatar-btn` | `profile/view-and-edit.yaml` | + +AppHeader snippet (line ~109): +```tsx + setMenuOpen(true)} + ... +> +``` + +--- + +## HIGH — SOS screen send button (no text, no testID) + +| Component | File | Recommended testID | Used by flow | +|---|---|---|---| +| Send/submit Pressable in chat input area | `app/urge.tsx` | `sos-send-btn` | `urge/sos-flow.yaml` | + +The send icon Pressable has no text and no testID. Current flow cannot reliably tap it. + +--- + +## HIGH — Demographics field row Pressables + +Each row in DemographicsAccordion that opens a WheelPickerModal is a Pressable with no testID. +Currently matched via hardcoded German label text (stable, but fragile if labels change). + +| Field | File | Recommended testID | +|---|---|---| +| Geburtsjahr row | `components/profile/DemographicsAccordion.tsx` | `demographics-birth-year-row` | +| Geschlecht row | `components/profile/DemographicsAccordion.tsx` | `demographics-gender-row` | +| Familienstand row | `components/profile/DemographicsAccordion.tsx` | `demographics-marital-row` | +| Berufsstatus row | `components/profile/DemographicsAccordion.tsx` | `demographics-employment-row` | +| Bundesland row | `components/profile/DemographicsAccordion.tsx` | `demographics-bundesland-row` | +| Stadt / Landkreis row | `components/profile/DemographicsAccordion.tsx` | `demographics-city-row` | + +--- + +## MEDIUM — Auth screen inputs (currently matched via i18n placeholder text) + +| Component | File | Recommended testID | Risk | +|---|---|---|---| +| Email TextInput | `app/(auth)/signin.tsx` line ~139 | `auth-email-input` | breaks if de.json placeholder changes | +| Password TextInput | `app/(auth)/signin.tsx` line ~151 | `auth-password-input` | same | +| Submit Pressable | `app/(auth)/signin.tsx` line ~173 | `auth-signin-btn` | breaks if t('auth.signin') changes | + +--- + +## MEDIUM — ProfileEdit screen nickname input + +| Component | File | Recommended testID | +|---|---|---| +| Nickname TextInput | `app/profile/edit.tsx` line ~295 | `profile-nickname-input` | +| Save Pressable | `app/profile/edit.tsx` line ~154 | `profile-save-btn` | + +--- + +## MEDIUM — ComposeCard share button + +| Component | File | Recommended testID | Risk | +|---|---|---|---| +| Share/Teilen Pressable | `components/ComposeCard.tsx` line ~165 | `compose-share-btn` | breaks if t('community.share') changes | + +--- + +## LOW — Settings screen rows + +Theme selection uses @react-native-menu/menu (native iOS UIMenu). +Maestro may not interact with native UIMenu popovers — coordinate taps on the anchor +Pressable are the only option without testID. If the native menu approach proves unreliable, +a fallback custom picker with testID would be needed. + +| Component | File | Recommended testID | +|---|---|---| +| Theme menu anchor Pressable | `app/settings.tsx` | `settings-theme-picker` | +| Language menu anchor Pressable | `app/settings.tsx` | `settings-language-picker` | + +--- + +## Notes + +- Adding testID to a Pressable/TextInput does NOT require any logic change — it is a + metadata prop only. Safe for UI agent to add without Ahmed review. +- RiveAvatar in urge.tsx: no testID needed — Maestro cannot assert animation states. + SOS screen is verified via the chat TextInput placeholder instead. +- NotificationsDropdown: not tested in current flow suite — no testID needed yet. diff --git a/apps/rebreak-native/.maestro/auth/email-signin.yaml b/apps/rebreak-native/.maestro/auth/email-signin.yaml new file mode 100644 index 0000000..01d8da5 --- /dev/null +++ b/apps/rebreak-native/.maestro/auth/email-signin.yaml @@ -0,0 +1,44 @@ +# auth/email-signin.yaml +# Tests: App starts → sign-in screen loads → email+password login succeeds → Home-Feed visible. +# Pre-requisite: App installed, E2E_TEST_USER account exists on staging backend. +# Env-Vars: E2E_TEST_USER (username without @rebreak.internal), E2E_TEST_PASSWORD +# Expected outcome: "ReBreak" headline visible in AppHeader after login. +# Note: No testIDs on signin inputs — text selectors match i18n keys from de.json. +# Run with --env=E2E_LOCALE=de if CI device locale may differ. + +appId: org.rebreak.app +--- +- launchApp: + clearState: true + +# Splash / font-load can take a moment +- waitForAnimationToEnd: + timeout: 5000 + +# Signin screen must appear immediately — no auth state after clearState. +# TextInput placeholder = t('auth.emailPlaceholder') = "E-Mail" (de.json) +- assertVisible: + text: "E-Mail" + +- tapOn: + text: "E-Mail" +- inputText: ${E2E_TEST_USER}@rebreak.internal + +# Password input placeholder = t('auth.passwordPlaceholder') = "Passwort" (de.json) +- tapOn: + text: "Passwort" +- inputText: ${E2E_TEST_PASSWORD} + +# Submit button text = t('auth.signin') = "Anmelden" (de.json) +# Button is disabled until both fields have content — typing above enables it. +- tapOn: + text: "Anmelden" + +# Supabase auth + /api/auth/me call — allow network round-trip +- waitForAnimationToEnd: + timeout: 10000 + +# AppHeader shows t('appHeader.appName') = "ReBreak" (hardcoded fallback, de.json). +# This text only appears in the authenticated Home layout. +- assertVisible: + text: "ReBreak" diff --git a/apps/rebreak-native/.maestro/community/create-post.yaml b/apps/rebreak-native/.maestro/community/create-post.yaml new file mode 100644 index 0000000..9f36c7a --- /dev/null +++ b/apps/rebreak-native/.maestro/community/create-post.yaml @@ -0,0 +1,72 @@ +# community/create-post.yaml +# Tests: Login → Home-Feed → ComposeCard → type text → publish → ComposeCard resets. +# Pre-requisite: App installed. Test-user exists on staging. +# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD +# Expected outcome: After tapping "Teilen", ComposeCard resets to idle state +# (placeholder text visible again). Post is created in staging DB. +# +# NOTE: This flow creates a real post on staging. No automatic cleanup. +# Delete via Service-Role or Supabase dashboard after test runs. +# +# ComposeCard placeholder = t('community.compose_placeholder') = "Was bewegt dich gerade?" (de.json) +# Share button = t('community.share') = "Teilen" (de.json) + +appId: org.rebreak.app +--- +- launchApp: + clearState: true + +- waitForAnimationToEnd: + timeout: 5000 + +# --- Auth --- +- assertVisible: + text: "E-Mail" +- tapOn: + text: "E-Mail" +- inputText: ${E2E_TEST_USER}@rebreak.internal +- tapOn: + text: "Passwort" +- inputText: ${E2E_TEST_PASSWORD} +- tapOn: + text: "Anmelden" +- waitForAnimationToEnd: + timeout: 10000 + +# --- Home-Feed --- +- assertVisible: + text: "ReBreak" + +# ComposeCard sits at top of Home feed (/(app)/index.tsx). +# Scroll up first to make sure we're at the top. +- scrollUntilVisible: + element: + text: "Was bewegt dich gerade?" + direction: UP + timeout: 4000 + +- assertVisible: + text: "Was bewegt dich gerade?" + +# Tap placeholder to focus the TextInput (sets focused=true, showActions=true) +- tapOn: + text: "Was bewegt dich gerade?" +- waitForAnimationToEnd: + timeout: 1000 + +# Type post content — unique enough to find in DB for cleanup +- inputText: "[E2E] Maestro-Testpost — bitte ignorieren." + +# After text input: action row appears with "Teilen" button +- assertVisible: + text: "Teilen" + +# Submit. API call: POST /api/community/post → returns 200 → queryClient invalidates +- tapOn: + text: "Teilen" +- waitForAnimationToEnd: + timeout: 8000 + +# After success: cancel() is called → content reset → ComposeCard shows placeholder again +- assertVisible: + text: "Was bewegt dich gerade?" diff --git a/apps/rebreak-native/.maestro/profile/demographics.yaml b/apps/rebreak-native/.maestro/profile/demographics.yaml new file mode 100644 index 0000000..82bd134 --- /dev/null +++ b/apps/rebreak-native/.maestro/profile/demographics.yaml @@ -0,0 +1,118 @@ +# profile/demographics.yaml +# Tests: Login → Profile → DemographicsAccordion toggle → WheelPicker opens for Geburtsjahr +# → dismiss → Gender picker opens → dismiss → verify accordion still expanded. +# Pre-requisite: App installed. Test-user exists on staging. +# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD +# Expected outcome: Accordion expands, pickers open without crash. +# +# LIMITATION: WheelPickerModal is a Modal + ScrollView — Maestro can tap within it +# but cannot reliably assert a specific wheel item is selected. This flow only verifies +# the UI path opens and dismisses cleanly (no crash). Full field-fill test requires +# testIDs on each picker trigger row. +# +# NOTE: The accordion header uses hardcoded text (not i18n): +# "ANONYMER BEITRAG ZUR FORSCHUNG" — safe to match as static string. +# +# BLOCKER on Avatar tap: coordinate-based (93%, 6%). See TODO_TESTIDS.md. + +appId: org.rebreak.app +--- +- launchApp: + clearState: true + +- waitForAnimationToEnd: + timeout: 5000 + +# --- Auth --- +- assertVisible: + text: "E-Mail" +- tapOn: + text: "E-Mail" +- inputText: ${E2E_TEST_USER}@rebreak.internal +- tapOn: + text: "Passwort" +- inputText: ${E2E_TEST_PASSWORD} +- tapOn: + text: "Anmelden" +- waitForAnimationToEnd: + timeout: 10000 + +- assertVisible: + text: "ReBreak" + +# Open dropdown → Profile +- tapOn: + point: "93%, 6%" +- waitForAnimationToEnd: + timeout: 2000 +- assertVisible: + text: "Profil" +- tapOn: + text: "Profil" +- waitForAnimationToEnd: + timeout: 4000 + +- assertVisible: + text: "Profil" + +# Scroll down to reach DemographicsAccordion (below StreakSection / UrgeStatsCard) +- scrollUntilVisible: + element: + text: "ANONYMER BEITRAG ZUR FORSCHUNG" + direction: DOWN + timeout: 6000 + +# Accordion header text is hardcoded — safe to use as selector. +- assertVisible: + text: "ANONYMER BEITRAG ZUR FORSCHUNG" + +# Tap accordion header to expand +- tapOn: + text: "ANONYMER BEITRAG ZUR FORSCHUNG" +- waitForAnimationToEnd: + timeout: 1500 + +# After expansion: the progress bar area and field rows appear. +# "Geburtsjahr" label is hardcoded in DemographicsAccordion. +# FRAGILE: no testID on the row Pressable. Using text selector on label. +- scrollUntilVisible: + element: + text: "Geburtsjahr" + direction: DOWN + timeout: 3000 +- assertVisible: + text: "Geburtsjahr" + +# Tap Geburtsjahr row to open WheelPickerModal +- tapOn: + text: "Geburtsjahr" +- waitForAnimationToEnd: + timeout: 2000 + +# WheelPickerModal is open. Dismiss by tapping the backdrop or close button. +# WheelPickerModal has no testID on the backdrop. Tap outside the picker area. +- tapOn: + point: "50%, 10%" +- waitForAnimationToEnd: + timeout: 1000 + +# Tap Geschlecht (Gender) row — also hardcoded label text in DemographicsAccordion +- scrollUntilVisible: + element: + text: "Geschlecht" + direction: DOWN + timeout: 3000 +- tapOn: + text: "Geschlecht" +- waitForAnimationToEnd: + timeout: 2000 + +# Dismiss gender picker +- tapOn: + point: "50%, 10%" +- waitForAnimationToEnd: + timeout: 1000 + +# Verify accordion is still expanded after dismissing pickers +- assertVisible: + text: "Geburtsjahr" diff --git a/apps/rebreak-native/.maestro/profile/view-and-edit.yaml b/apps/rebreak-native/.maestro/profile/view-and-edit.yaml new file mode 100644 index 0000000..914b298 --- /dev/null +++ b/apps/rebreak-native/.maestro/profile/view-and-edit.yaml @@ -0,0 +1,113 @@ +# profile/view-and-edit.yaml +# Tests: Login → open Header dropdown → tap "Profil" → ProfileScreen loads → +# tap edit (navigates to /profile/edit) → change nickname → tap Save. +# Pre-requisite: App installed. Test-user account exists on staging. +# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD +# Expected outcome: Save button (t('profile.edit_save') = "Speichern") tapped, +# screen returns to ProfileScreen without error. +# +# BLOCKER: Avatar Pressable in AppHeader has NO testID and NO accessibilityLabel. +# The dropdown open-tap uses a coordinate fallback (93%, 6%). This breaks if +# header layout changes. See TODO_TESTIDS.md — needs testID="header-avatar-btn". + +appId: org.rebreak.app +--- +- launchApp: + clearState: true + +- waitForAnimationToEnd: + timeout: 5000 + +# --- Auth --- +- assertVisible: + text: "E-Mail" +- tapOn: + text: "E-Mail" +- inputText: ${E2E_TEST_USER}@rebreak.internal +- tapOn: + text: "Passwort" +- inputText: ${E2E_TEST_PASSWORD} +- tapOn: + text: "Anmelden" +- waitForAnimationToEnd: + timeout: 10000 + +# --- Home --- +- assertVisible: + text: "ReBreak" + +# Open HeaderDropdownMenu via Avatar tap (top-right corner). +# FRAGILE — needs testID="header-avatar-btn" to become stable. See TODO_TESTIDS.md. +- tapOn: + point: "93%, 6%" +- waitForAnimationToEnd: + timeout: 2000 + +# Dropdown label = t('headerMenu.profile') = "Profil" (de.json) +- assertVisible: + text: "Profil" +- tapOn: + text: "Profil" +- waitForAnimationToEnd: + timeout: 4000 + +# ProfileScreen: AppHeader title is hardcoded "Profil" in ProfileScreen. +# "Posts" label in StatsBar (hardcoded, no i18n) confirms the screen rendered. +- assertVisible: + text: "Profil" +- assertVisible: + text: "Posts" + +# Scroll down to make sure StatsBar area is visible before continuing +- scrollUntilVisible: + element: + text: "Posts" + direction: DOWN + timeout: 3000 + +# Navigate to edit screen. +# ProfileHeader has two Pressables: onEditAvatar + onEditNickname — both push /profile/edit. +# The camera icon area has no testID. We scroll back up first and tap the nickname area. +# FRAGILE — needs testID="profile-edit-nickname-btn" on the nickname Pressable. +# See TODO_TESTIDS.md. +- scrollUntilVisible: + element: + text: "Profil" + direction: UP + timeout: 3000 +- tapOn: + point: "50%, 28%" + +- waitForAnimationToEnd: + timeout: 3000 + +# Edit screen: title = t('profile.edit_title') — check SETUP.md for de.json value. +# Nickname TextInput has no testID. Tap and clear existing value, type new one. +# We use scrollUntilVisible to reach the nickname section. +- scrollUntilVisible: + element: + text: "NICKNAME" + direction: DOWN + timeout: 3000 + +# Nickname TextInput has no testID. Tap the placeholder text (t('auth.nicknamePlaceholder')) +# if field is empty, or tap directly below the "NICKNAME" label. +# clearText clears whatever is in the focused input. +- tapOn: + text: "NICKNAME" +- waitForAnimationToEnd: + timeout: 500 +# The TextInput sits directly below the NICKNAME label — Maestro focuses the last +# tapped element. If placeholder is visible, tap it instead. +- clearText +- inputText: "TestNick" + +# Save button = t('profile.edit_save') = "Speichern" (de.json). Only active when hasChanges. +- tapOn: + text: "Speichern" +- waitForAnimationToEnd: + timeout: 5000 + +# After save: router.back() → ProfileScreen. "Profil" title visible again. +- assertVisible: + text: "Profil" diff --git a/apps/rebreak-native/.maestro/settings/dark-theme.yaml b/apps/rebreak-native/.maestro/settings/dark-theme.yaml new file mode 100644 index 0000000..a158ccd --- /dev/null +++ b/apps/rebreak-native/.maestro/settings/dark-theme.yaml @@ -0,0 +1,85 @@ +# settings/dark-theme.yaml +# Tests: Login → Header dropdown → Settings → Theme picker → Dark mode selected. +# Pre-requisite: App installed. Test-user exists on staging. +# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD +# Expected outcome: After selecting "Dunkel", Settings screen reflects dark theme +# (background color changes — Maestro cannot assert CSS, but +# assertVisible on a dark-only element or the "Dunkel" value chip +# confirms the selection was persisted in local store). +# +# IMPORTANT: Theme is stored in MMKV (useThemeStore). clearState: true resets it +# to 'system' each run — so this flow always starts from system default. +# +# The theme menu is a @react-native-menu/menu MenuView (native iOS context menu). +# Maestro can trigger the anchor Pressable but may NOT be able to interact with +# the native UIMenu popover on iOS. If this step fails, it is a Maestro limitation +# with native context menus — add to SETUP.md known issues. +# Alternative: implement a test-only theme toggle button accessible via testID. +# See TODO_TESTIDS.md. + +appId: org.rebreak.app +--- +- launchApp: + clearState: true + +- waitForAnimationToEnd: + timeout: 5000 + +# --- Auth --- +- assertVisible: + text: "E-Mail" +- tapOn: + text: "E-Mail" +- inputText: ${E2E_TEST_USER}@rebreak.internal +- tapOn: + text: "Passwort" +- inputText: ${E2E_TEST_PASSWORD} +- tapOn: + text: "Anmelden" +- waitForAnimationToEnd: + timeout: 10000 + +- assertVisible: + text: "ReBreak" + +# Open Header dropdown → Settings +- tapOn: + point: "93%, 6%" +- waitForAnimationToEnd: + timeout: 2000 + +# t('headerMenu.settings') = "Einstellungen" (de.json) +- assertVisible: + text: "Einstellungen" +- tapOn: + text: "Einstellungen" +- waitForAnimationToEnd: + timeout: 3000 + +# Settings screen: AppHeader title = t('settings.title') = "Einstellungen" +- assertVisible: + text: "Einstellungen" + +# Theme section: t('settings.theme') = "Erscheinungsbild" (de.json) — row label. +# The current value chip shows t('settings.theme_system') = "Systemstandard" initially. +- assertVisible: + text: "Erscheinungsbild" + +# Tap the value chip (MenuView anchor Pressable) to open iOS UIMenu. +# WARNING: Native UIMenu interaction may not work in Maestro — see header note. +# We tap on the current value "Systemstandard" which is inside the anchor Pressable. +- tapOn: + text: "Systemstandard" +- waitForAnimationToEnd: + timeout: 1500 + +# iOS native UIMenu: items are "Systemstandard", "Hell", "Dunkel" +# t('settings.theme_dark') = "Dunkel" (de.json) +- tapOn: + text: "Dunkel" +- waitForAnimationToEnd: + timeout: 1500 + +# After selection: the value chip in the row should now show "Dunkel" +- assertVisible: + text: "Dunkel" diff --git a/apps/rebreak-native/.maestro/urge/sos-flow.yaml b/apps/rebreak-native/.maestro/urge/sos-flow.yaml new file mode 100644 index 0000000..917f482 --- /dev/null +++ b/apps/rebreak-native/.maestro/urge/sos-flow.yaml @@ -0,0 +1,94 @@ +# urge/sos-flow.yaml +# Tests: Login → Header dropdown → SOS → Lyra chat screen loads → breathing exercise +# triggered → session ends → feedback rating drawer appears. +# Pre-requisite: App installed. Test-user exists on staging. Staging backend running. +# Groq API key configured (SOS streams via /api/coach/sos-stream). +# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD +# Expected outcome: Lyra chat input visible after SOS screen loads. +# Breathing drawer opens when "Atemübung" chip is tapped. +# +# TIMING NOTE: SOS screen boots RiveAvatar + streams first Lyra message via Groq. +# Timeout of 12s post-navigation is intentional — cold LLM response on staging. +# +# BLOCKER: Avatar button in AppHeader has no testID → coordinate fallback (93%, 6%). +# SOS-entry Pressable in HeaderDropdownMenu has no testID → text "SOS" selector works +# because "SOS" only appears inside the open dropdown. + +appId: org.rebreak.app +--- +- launchApp: + clearState: true + +- waitForAnimationToEnd: + timeout: 5000 + +# --- Auth --- +- assertVisible: + text: "E-Mail" +- tapOn: + text: "E-Mail" +- inputText: ${E2E_TEST_USER}@rebreak.internal +- tapOn: + text: "Passwort" +- inputText: ${E2E_TEST_PASSWORD} +- tapOn: + text: "Anmelden" +- waitForAnimationToEnd: + timeout: 10000 + +- assertVisible: + text: "ReBreak" + +# Open Header dropdown +- tapOn: + point: "93%, 6%" +- waitForAnimationToEnd: + timeout: 2000 + +# t('appHeader.sosLabel') = "SOS" (de.json) — red text, top of dropdown +- assertVisible: + text: "SOS" +- tapOn: + text: "SOS" + +# SOS screen loads: RiveAvatar renders + Lyra streaming starts. +# Boot takes 6-12s on staging (Groq cold start + audio pre-cache). +- waitForAnimationToEnd: + timeout: 12000 + +# Chat input: placeholder = t('coach.placeholder') = "Was beschäftigt dich?" (de.json). +# This placeholder ONLY exists on the SOS/Urge screen. +- assertVisible: + text: "Was beschäftigt dich?" + +# Type a message to trigger Lyra response + chip suggestions +- tapOn: + text: "Was beschäftigt dich?" +- inputText: "Ich habe gerade einen starken Drang." +- tapOn: + text: "Was beschäftigt dich?" + +# After typing, a send button should appear (Ionicons send icon, no text/testID). +# Tap on the text input area then look for the send button via coordinates. +# FRAGILE — needs testID="sos-send-btn" on the send Pressable. See TODO_TESTIDS.md. +# Instead of sending (which triggers another streaming round-trip), +# we verify the chip row is visible if Lyra has already responded. +# If this is the first message, wait for Lyra's response + chip appearance. +- waitForAnimationToEnd: + timeout: 15000 + +# After Lyra responds, chipSet changes from 'start' to dynamic chips. +# "Atemübung" is a chip in CHIP_SETS.start (sosConstants.ts) — always shown first. +# Text is hardcoded in sosConstants.ts, not i18n. +# If visible: tap to open BreathingDrawer. +- assertVisible: + text: "Atemübung" +- tapOn: + text: "Atemübung" +- waitForAnimationToEnd: + timeout: 3000 + +# BreathingDrawer opens as a bottom sheet. Header text is hardcoded +# "Atemübung" in Breathing.tsx — safe to assert. +- assertVisible: + text: "Atemübung"