From 1a73837ce0c2b88a54586a94f3dff8341a41ab0b Mon Sep 17 00:00:00 2001 From: Emoji-dot Date: Wed, 24 Jun 2026 19:26:44 +0100 Subject: [PATCH] Add E2E test coverage for core mobile flows --- .github/workflows/e2e-tests.yml | 150 +++++++ .gitignore | 4 + .maestro/01-onboarding-to-profile.yaml | 16 + .maestro/02-wallet-entry.yaml | 21 + .maestro/03-guild-navigation.yaml | 35 ++ .maestro/04-access-check-success.yaml | 39 ++ .maestro/05-access-check-failure.yaml | 39 ++ .maestro/06-reset-app-state.yaml | 33 ++ .maestro/CONTRIBUTING_CHECKLIST.md | 174 +++++++ .maestro/QUICKSTART.md | 124 +++++ .maestro/README.md | 224 +++++++++ .maestro/config.yaml | 15 + .vscode/settings.json | 2 + README.md | 43 ++ app/access-check.tsx | 11 +- app/guilds.tsx | 3 +- app/guilds/[guildId].tsx | 15 +- app/onboarding.tsx | 8 +- app/profile.tsx | 16 +- app/settings.tsx | 9 +- docs/E2E_IMPLEMENTATION_SUMMARY.md | 250 +++++++++++ docs/e2e-testing-guide.md | 598 +++++++++++++++++++++++++ package-lock.json | 375 +++++----------- package.json | 7 +- src/components/Button.tsx | 3 + src/components/WalletInput.tsx | 3 + 26 files changed, 1936 insertions(+), 281 deletions(-) create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 .maestro/01-onboarding-to-profile.yaml create mode 100644 .maestro/02-wallet-entry.yaml create mode 100644 .maestro/03-guild-navigation.yaml create mode 100644 .maestro/04-access-check-success.yaml create mode 100644 .maestro/05-access-check-failure.yaml create mode 100644 .maestro/06-reset-app-state.yaml create mode 100644 .maestro/CONTRIBUTING_CHECKLIST.md create mode 100644 .maestro/QUICKSTART.md create mode 100644 .maestro/README.md create mode 100644 .maestro/config.yaml create mode 100644 .vscode/settings.json create mode 100644 docs/E2E_IMPLEMENTATION_SUMMARY.md create mode 100644 docs/e2e-testing-guide.md diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..c3f8f49 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,150 @@ +name: E2E Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + workflow_dispatch: + +jobs: + e2e-ios: + name: E2E Tests (iOS) + runs-on: macos-latest + timeout-minutes: 45 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Maestro + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + - name: Setup Expo + uses: expo/expo-github-action@v8 + with: + expo-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Build iOS app + run: | + npx expo prebuild --platform ios --clean + xcodebuild -workspace ios/guildpassmobile.xcworkspace \ + -scheme guildpassmobile \ + -configuration Debug \ + -sdk iphonesimulator \ + -derivedDataPath ios/build \ + -destination 'platform=iOS Simulator,name=iPhone 15' + + - name: Boot iOS Simulator + run: | + xcrun simctl boot "iPhone 15" || true + xcrun simctl list devices + + - name: Install app on simulator + run: | + xcrun simctl install booted ios/build/Build/Products/Debug-iphonesimulator/guildpassmobile.app + + - name: Run Maestro E2E tests + run: | + maestro test .maestro/ --format junit --output test-results + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results-ios + path: test-results/ + + - name: Upload Maestro recordings + if: always() + uses: actions/upload-artifact@v4 + with: + name: maestro-recordings-ios + path: ~/.maestro/tests/**/*.mp4 + + e2e-android: + name: E2E Tests (Android) + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install Maestro + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + - name: Setup Expo + uses: expo/expo-github-action@v8 + with: + expo-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Setup Android Emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 33 + target: google_apis + arch: x86_64 + profile: pixel_6 + script: echo "Emulator started" + + - name: Build Android app + run: | + npx expo prebuild --platform android --clean + cd android + ./gradlew assembleDebug + + - name: Run Maestro E2E tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 33 + target: google_apis + arch: x86_64 + profile: pixel_6 + script: | + adb install android/app/build/outputs/apk/debug/app-debug.apk + maestro test .maestro/ --format junit --output test-results + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results-android + path: test-results/ + + - name: Upload Maestro recordings + if: always() + uses: actions/upload-artifact@v4 + with: + name: maestro-recordings-android + path: ~/.maestro/tests/**/*.mp4 diff --git a/.gitignore b/.gitignore index 1727f92..7f6d5e6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ yarn-error.log* .env .env.* !.env.example + +# Maestro E2E test results and recordings +test-results/ +*.mp4 diff --git a/.maestro/01-onboarding-to-profile.yaml b/.maestro/01-onboarding-to-profile.yaml new file mode 100644 index 0000000..204a4b0 --- /dev/null +++ b/.maestro/01-onboarding-to-profile.yaml @@ -0,0 +1,16 @@ +appId: xyz.guildpass.mobile +--- +# E2E Test: Onboarding to Profile Navigation +# Validates: User can complete onboarding and navigate to profile screen + +- launchApp +- assertVisible: + id: "onboarding-screen" +- assertVisible: + text: "Welcome to GuildPass" +- tapOn: + id: "onboarding-get-started-button" +- assertVisible: + id: "profile-screen" +- assertVisible: + text: "Connect Wallet" diff --git a/.maestro/02-wallet-entry.yaml b/.maestro/02-wallet-entry.yaml new file mode 100644 index 0000000..bb816fa --- /dev/null +++ b/.maestro/02-wallet-entry.yaml @@ -0,0 +1,21 @@ +appId: xyz.guildpass.mobile +--- +# E2E Test: Manual Wallet Entry +# Validates: User can manually enter a wallet address and connect + +- launchApp +- tapOn: + id: "onboarding-get-started-button" +- assertVisible: + id: "wallet-connect-form" +- tapOn: + id: "wallet-address-input" +- inputText: "0x1234567890123456789012345678901234567890" +- tapOn: + id: "wallet-connect-button" +- assertVisible: + id: "profile-connected" +- assertVisible: + id: "connected-wallet-address" +- assertVisible: + text: "0x1234567890123456789012345678901234567890" diff --git a/.maestro/03-guild-navigation.yaml b/.maestro/03-guild-navigation.yaml new file mode 100644 index 0000000..f7345c8 --- /dev/null +++ b/.maestro/03-guild-navigation.yaml @@ -0,0 +1,35 @@ +appId: xyz.guildpass.mobile +--- +# E2E Test: Guild List and Detail Navigation +# Validates: User can navigate to guilds list and view guild details + +- launchApp +- tapOn: + id: "onboarding-get-started-button" +- tapOn: + id: "wallet-address-input" +- inputText: "0x1234567890123456789012345678901234567890" +- tapOn: + id: "wallet-connect-button" +- assertVisible: + id: "profile-connected" + +# Navigate to guilds list +- tapOn: + id: "navigate-guilds-button" +- assertVisible: + id: "guilds-screen" +- assertVisible: + id: "guilds-list" + +# Tap on first guild card +- tapOn: + text: "Alpha Guild" +- assertVisible: + id: "guild-detail-screen" +- assertVisible: + id: "guild-name" +- assertVisible: + id: "membership-status-card" +- assertVisible: + id: "guild-roles-list" diff --git a/.maestro/04-access-check-success.yaml b/.maestro/04-access-check-success.yaml new file mode 100644 index 0000000..0ffe2f4 --- /dev/null +++ b/.maestro/04-access-check-success.yaml @@ -0,0 +1,39 @@ +appId: xyz.guildpass.mobile +env: + MOCK_API_SUCCESS: true +--- +# E2E Test: Access Check Success Path +# Validates: User can perform an access check with successful result +# Note: Requires mock API to return success response + +- launchApp +- tapOn: + id: "onboarding-get-started-button" +- tapOn: + id: "wallet-address-input" +- inputText: "0x1234567890123456789012345678901234567890" +- tapOn: + id: "wallet-connect-button" + +# Navigate to access check +- tapOn: + id: "navigate-access-check-button" +- assertVisible: + id: "access-check-screen" + +# Fill in access check form +- tapOn: + id: "access-check-guild-id-input" +- inputText: "guild_abc" +- tapOn: + id: "access-check-resource-id-input" +- inputText: "vip-door" + +# Submit access check +- tapOn: + id: "check-access-button" + +# Wait for result +- assertVisible: + id: "access-check-result" + timeout: 10000 diff --git a/.maestro/05-access-check-failure.yaml b/.maestro/05-access-check-failure.yaml new file mode 100644 index 0000000..9e1ccc7 --- /dev/null +++ b/.maestro/05-access-check-failure.yaml @@ -0,0 +1,39 @@ +appId: xyz.guildpass.mobile +env: + MOCK_API_FAILURE: true +--- +# E2E Test: Access Check Failure Path +# Validates: User sees appropriate error when access check fails +# Note: Requires mock API to return error response + +- launchApp +- tapOn: + id: "onboarding-get-started-button" +- tapOn: + id: "wallet-address-input" +- inputText: "0x0000000000000000000000000000000000000000" +- tapOn: + id: "wallet-connect-button" + +# Navigate to access check +- tapOn: + id: "navigate-access-check-button" +- assertVisible: + id: "access-check-screen" + +# Fill in access check form with invalid data +- tapOn: + id: "access-check-guild-id-input" +- inputText: "invalid_guild" +- tapOn: + id: "access-check-resource-id-input" +- inputText: "invalid_resource" + +# Submit access check +- tapOn: + id: "check-access-button" + +# Wait for error +- assertVisible: + id: "access-check-error" + timeout: 10000 diff --git a/.maestro/06-reset-app-state.yaml b/.maestro/06-reset-app-state.yaml new file mode 100644 index 0000000..6116396 --- /dev/null +++ b/.maestro/06-reset-app-state.yaml @@ -0,0 +1,33 @@ +appId: xyz.guildpass.mobile +--- +# E2E Test: Reset App State +# Validates: User can reset app state from settings screen + +- launchApp +- tapOn: + id: "onboarding-get-started-button" +- tapOn: + id: "wallet-address-input" +- inputText: "0x1234567890123456789012345678901234567890" +- tapOn: + id: "wallet-connect-button" +- assertVisible: + id: "profile-connected" + +# Navigate to settings +- tapOn: + id: "navigate-settings-button" +- assertVisible: + id: "settings-screen" +- assertVisible: + id: "settings-api-url" +- assertVisible: + id: "settings-chain-id" + +# Reset app state +- tapOn: + id: "reset-app-state-button" + +# Should return to disconnected state +- assertVisible: + id: "wallet-connect-form" diff --git a/.maestro/CONTRIBUTING_CHECKLIST.md b/.maestro/CONTRIBUTING_CHECKLIST.md new file mode 100644 index 0000000..4b67116 --- /dev/null +++ b/.maestro/CONTRIBUTING_CHECKLIST.md @@ -0,0 +1,174 @@ +# E2E Testing Contribution Checklist + +Use this checklist when adding new features or modifying existing screens. + +## ✅ Adding a New Screen + +- [ ] Add `testID` prop to screen container + ```tsx + + ``` + +- [ ] Add `testID` props to all interactive elements + - [ ] Buttons: `{action}-button` + - [ ] Inputs: `{field-name}-input` + - [ ] Navigation items: `navigate-{destination}-button` + +- [ ] Create Maestro test flow file + - [ ] Name: `.maestro/XX-feature-name.yaml` + - [ ] Include descriptive comments + - [ ] Test happy path + - [ ] Test error scenarios + +- [ ] Add flow to config + - [ ] Update `.maestro/config.yaml` + +- [ ] Update documentation + - [ ] Add entry to `.maestro/README.md` + - [ ] Update test coverage section + +- [ ] Test locally + - [ ] Run: `maestro test .maestro/XX-feature-name.yaml` + - [ ] Verify on iOS + - [ ] Verify on Android (if possible) + +- [ ] Verify CI passes + - [ ] Check GitHub Actions after pushing + +## ✅ Modifying Existing Screen + +- [ ] Check if testID props changed + - [ ] If yes, update corresponding Maestro flows + +- [ ] Check if user flow changed + - [ ] If yes, update test steps + +- [ ] Run affected tests + ```bash + maestro test .maestro/ + ``` + +- [ ] Verify no regressions + - [ ] All existing tests should still pass + +## ✅ Adding Interactive Component + +When creating a new reusable component: + +- [ ] Add `testID` prop to component interface + ```tsx + interface MyComponentProps { + testID?: string; + // ... other props + } + ``` + +- [ ] Pass `testID` to root element + ```tsx + + ``` + +- [ ] Document testID usage in component comments + ```tsx + /** + * @param testID - Optional test identifier for E2E tests + */ + ``` + +## ✅ Pull Request Checklist + +Before opening PR: + +- [ ] All new components have testID support +- [ ] Test flows created for new features +- [ ] Tests pass locally +- [ ] Documentation updated +- [ ] Ran `npm run lint` and `npm run typecheck` +- [ ] Added E2E test coverage note in PR description + +In PR description, include: +```markdown +## E2E Test Coverage + +- [ ] Added testID props to: [list components] +- [ ] Created Maestro flows: [list files] +- [ ] Tests passing locally: ✅ +- [ ] Test coverage: [describe what's tested] +``` + +## ✅ Code Review Checklist + +When reviewing PRs: + +- [ ] Verify testID props follow naming convention +- [ ] Check if Maestro flows are needed +- [ ] Confirm tests are added if UI changes +- [ ] Review test coverage completeness +- [ ] Check CI test results + +## Test ID Naming Reference + +| Element Type | Pattern | Example | +|--------------|---------|---------| +| Screen | `{name}-screen` | `profile-screen` | +| Button | `{action}-button` | `submit-button` | +| Input | `{field}-input` | `email-input` | +| Navigation | `navigate-{dest}-button` | `navigate-settings-button` | +| Status | `{feature}-{state}` | `loading-indicator` | +| Error | `{feature}-error` | `login-error` | +| Success | `{feature}-success` | `payment-success` | + +## Common Mistakes to Avoid + +❌ **Don't**: Use generic IDs +```tsx +testID="button1" +testID="input" +``` + +✅ **Do**: Use descriptive IDs +```tsx +testID="wallet-connect-button" +testID="email-input" +``` + +❌ **Don't**: Reuse IDs across screens +```tsx +// Screen A +