# Maestro E2E Testing Strategy for TabataFit ## Executive Summary **Maestro** is a mobile UI testing framework that uses YAML-based test flows. It's ideal for TabataFit because: - ✅ Declarative YAML syntax (no code required) - ✅ Built-in support for React Native - ✅ Handles animations and async operations gracefully - ✅ Excellent for regression testing critical user flows - ✅ Can run on physical devices and simulators --- ## Prerequisites Before implementing these tests, ensure the following features are complete: ### Required Features (Implement First) - [ ] Onboarding flow (5 screens + paywall) - [ ] Workout player with timer controls - [ ] Browse/Workouts tab with workout cards - [ ] Category filtering (Full Body, Core, Cardio, etc.) - [ ] Collections feature - [ ] Trainer profiles - [ ] Subscription/paywall integration - [ ] Workout completion screen - [ ] Profile/settings screen ### Nice to Have (Can Add Later) - [ ] Activity history tracking - [ ] Offline mode support - [ ] Deep linking - [ ] Push notifications --- ## Priority Test Flows ### **P0 - Critical Flows (Must Test Every Release)** 1. **Onboarding → First Workout** 2. **Browse → Select Workout → Play → Complete** 3. **Subscription Purchase Flow** 4. **Background/Foreground During Workout** ### **P1 - High Priority** 5. **Category Filtering** 6. **Collection Navigation** 7. **Trainer Workout Discovery** 8. **Profile Settings & Data Persistence** ### **P2 - Medium Priority** 9. **Activity History Tracking** 10. **Offline Mode Behavior** 11. **Deep Linking** 12. **Push Notifications** --- ## Test Suite Structure ``` .maestro/ ├── config.yaml # Global test configuration ├── flows/ │ ├── critical/ # P0 flows - Run on every PR │ │ ├── onboarding.yaml │ │ ├── workoutComplete.yaml │ │ └── subscription.yaml │ ├── core/ # P1 flows - Run before release │ │ ├── browseAndPlay.yaml │ │ ├── categoryFilter.yaml │ │ ├── collections.yaml │ │ └── trainers.yaml │ └── regression/ # P2 flows - Run nightly │ ├── activityHistory.yaml │ ├── offlineMode.yaml │ └── settings.yaml ├── helpers/ │ ├── common.yaml # Shared test steps │ ├── assertions.yaml # Custom assertions │ └── mock-data.yaml # Test data └── environments/ ├── staging.yaml └── production.yaml ``` --- ## Installation & Setup ### 1. Install Maestro CLI ```bash # macOS/Linux curl -Ls "https://get.maestro.mobile.dev" | bash # Verify installation maestro --version ``` ### 2. Setup Test Directory Structure ```bash mkdir -p .maestro/flows/{critical,core,regression} mkdir -p .maestro/helpers mkdir -p .maestro/environments ``` ### 3. Maestro Configuration (`config.yaml`) ```yaml # .maestro/config.yaml appId: com.tabatafit.app name: TabataFit E2E Tests # Timeouts timeout: 30000 # 30 seconds default retries: 2 # Environment variables env: TEST_USER_NAME: "Test User" TEST_USER_EMAIL: "test@example.com" # Include flows include: - flows/critical/*.yaml - flows/core/*.yaml # Exclude on CI exclude: - flows/regression/offlineMode.yaml # Requires airplane mode ``` --- ## P0 Critical Test Flows ### Test 1: Complete Onboarding Flow **File:** `.maestro/flows/critical/onboarding.yaml` ```yaml appId: com.tabatafit.app name: Complete Onboarding & First Workout onFlowStart: - clearState steps: # Splash/Loading - waitForAnimationToEnd: timeout: 5000 # Screen 1: Problem - "Not Enough Time" - assertVisible: "Not enough time" - tapOn: "Continue" # Screen 2: Empathy - assertVisible: "We get it" - tapOn: "Continue" # Screen 3: Solution - assertVisible: "4-minute workouts" - tapOn: "Continue" # Screen 4: Wow Moment - assertVisible: "Transform your body" - tapOn: "Get Started" # Screen 5: Personalization - tapOn: "Name input" - inputText: "Test User" - tapOn: "Beginner" - tapOn: "Lose Weight" - tapOn: "3 times per week" - tapOn: "Start My Journey" # Screen 6: Paywall (or skip in test env) - runScript: | if (maestro.env.SKIP_PAYWALL === 'true') { maestro.tapOn('Maybe Later'); } # Should land on Home - assertVisible: "Good morning|Good afternoon|Good evening" - assertVisible: "Test User" onFlowComplete: - takeScreenshot: "onboarding-complete" ``` ### Test 2: Browse, Select, and Complete Workout **File:** `.maestro/flows/critical/workoutComplete.yaml` ```yaml appId: com.tabatafit.app name: Browse, Play & Complete Workout steps: # Navigate to Workouts tab - tapOn: "Workouts" - waitForAnimationToEnd # Wait for data to load - assertVisible: "All|Full Body|Core|Upper Body" # Select first workout - tapOn: id: "workout-card-0" optional: false # Workout Detail Screen - assertVisible: "Start Workout" - tapOn: "Start Workout" # Player Screen - waitForAnimationToEnd: timeout: 3000 # Verify timer is running - assertVisible: "Get Ready|WORK|REST" # Fast-forward through workout (simulation) - repeat: times: 3 commands: - waitForAnimationToEnd: timeout: 5000 - assertVisible: "WORK|REST" # Complete workout - tapOn: id: "done-button" optional: true # Complete Screen - assertVisible: "Workout Complete|Great Job" - assertVisible: "Calories" - assertVisible: "Duration" # Return to home - tapOn: "Done|Continue" - assertVisible: "Home|Workouts" onFlowComplete: - takeScreenshot: "workout-completed" ``` ### Test 3: Subscription Purchase Flow **File:** `.maestro/flows/critical/subscription.yaml` ```yaml appId: com.tabatafit.app name: Subscription Purchase Flow steps: # Trigger paywall (via profile or workout limit) - tapOn: "Profile" - tapOn: "Upgrade to Premium" # Paywall Screen - assertVisible: "Unlock Everything|Premium" - assertVisible: "yearly|monthly" # Select plan - tapOn: id: "yearly-plan" # Verify Apple Pay/Google Pay sheet appears - assertVisible: "Subscribe|Confirm" # Note: Actual purchase is mocked in test env - runScript: | if (maestro.env.USE_MOCK_PURCHASE === 'true') { maestro.tapOn('Mock Purchase Success'); } # Verify premium activated - assertVisible: "Premium Active|You're all set" tags: - purchase - revenue-critical ``` --- ## P1 Core Test Flows ### Test 4: Category Filtering **File:** `.maestro/flows/core/categoryFilter.yaml` ```yaml appId: com.tabatafit.app name: Category Filtering steps: - tapOn: "Workouts" - waitForAnimationToEnd # Test each category - tapOn: "Full Body" - assertVisible: "Full Body" - tapOn: "Core" - assertVisible: "Core" - tapOn: "Cardio" - assertVisible: "Cardio" - tapOn: "All" - assertVisible: "All Workouts" # Verify filter changes content - runScript: | const before = maestro.getElementText('workout-count'); maestro.tapOn('Core'); const after = maestro.getElementText('workout-count'); assert(before !== after, 'Filter should change workout count'); ``` ### Test 5: Collection Navigation **File:** `.maestro/flows/core/collections.yaml` ```yaml appId: com.tabatafit.app name: Collection Navigation steps: - tapOn: "Browse" - waitForAnimationToEnd # Scroll to collections - swipe: direction: UP duration: 1000 # Tap first collection - tapOn: id: "collection-card-0" # Collection Detail Screen - assertVisible: "Collection" - assertVisible: "workouts" # Start collection workout - tapOn: "Start" - assertVisible: "Player|Timer" onFlowComplete: - takeScreenshot: "collection-navigation" ``` ### Test 6: Trainer Discovery **File:** `.maestro/flows/core/trainers.yaml` ```yaml appId: com.tabatafit.app name: Trainer Discovery steps: - tapOn: "Browse" # Navigate to trainers section - swipe: direction: UP # Select trainer - tapOn: id: "trainer-card-0" # Verify trainer profile - assertVisible: "workouts" # Select trainer's workout - tapOn: id: "workout-card-0" # Should show workout detail - assertVisible: "Start Workout" ``` --- ## Reusable Test Helpers ### Common Actions (`helpers/common.yaml`) ```yaml # .maestro/helpers/common.yaml appId: com.tabatafit.app # Launch app fresh - launchApp: clearState: true # Wait for data loading - waitForDataLoad: commands: - waitForAnimationToEnd: timeout: 3000 - assertVisible: ".*" # Any content loaded # Handle permission dialogs - handlePermissions: commands: - tapOn: text: "Allow" optional: true - tapOn: text: "OK" optional: true # Navigate to tab - navigateToTab: params: tabName: ${tab} commands: - tapOn: ${tab} # Start workout from detail - startWorkout: commands: - tapOn: "Start Workout" - waitForAnimationToEnd: timeout: 5000 - assertVisible: "Get Ready|WORK" ``` --- ## Running Tests ### Local Development ```bash # Install Maestro curl -Ls "https://get.maestro.mobile.dev" | bash # Run single test maestro test .maestro/flows/critical/onboarding.yaml # Run all critical tests maestro test .maestro/flows/critical/ # Run with specific environment maestro test --env SKIP_PAYWALL=true .maestro/flows/ # Record video of test maestro record .maestro/flows/critical/workoutComplete.yaml # Run with tags maestro test --include-tags=critical .maestro/flows/ ``` ### CI/CD Integration (GitHub Actions) ```yaml # .github/workflows/maestro.yml name: Maestro E2E Tests on: push: branches: [main] pull_request: branches: [main] jobs: e2e-tests: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 - name: Install Maestro run: curl -Ls "https://get.maestro.mobile.dev" | bash - name: Start iOS Simulator run: | xcrun simctl boot "iPhone 15" sleep 10 - name: Install App run: | npm install npx expo prebuild npx pod install npx react-native run-ios --simulator="iPhone 15" - name: Run Critical Tests run: | export MAESTRO_DRIVER_STARTUP_TIMEOUT=120000 maestro test .maestro/flows/critical/ - name: Upload Test Results if: always() uses: actions/upload-artifact@v3 with: name: maestro-results path: | ~/.maestro/tests/ *.png ``` --- ## Test Coverage Matrix | Feature | Test File | Priority | Frequency | Status | |---------|-----------|----------|-----------|--------| | Onboarding | `onboarding.yaml` | P0 | Every PR | ⏳ Pending | | Workout Play | `workoutComplete.yaml` | P0 | Every PR | ⏳ Pending | | Purchase | `subscription.yaml` | P0 | Every PR | ⏳ Pending | | Category Filter | `categoryFilter.yaml` | P1 | Pre-release | ⏳ Pending | | Collections | `collections.yaml` | P1 | Pre-release | ⏳ Pending | | Trainers | `trainers.yaml` | P1 | Pre-release | ⏳ Pending | | Activity | `activityHistory.yaml` | P2 | Nightly | ⏳ Pending | | Offline | `offlineMode.yaml` | P2 | Weekly | ⏳ Pending | --- ## React Native Prerequisites Before running tests, add `testID` props to components for reliable selectors: ```tsx // WorkoutCard.tsx {/* ... */} // WorkoutPlayer.tsx