Feat: Lightweight e2e tests for iOS and Android (#840)

* Add Maestro e2e testing

* Run Maestro flows in parallel

* Fix mobile e2e workflow

* Fix e2e script flow path

* prettier

* fix

* prettier

* standardize yml files and new formatting commands

* fix ndk

* fix exclusions

* use double quotes for yml files

* feedback

* fixes

* fixes

* fix

* fix ios job

* unneeded

* fix workflows

* fix launch workflow

* fix

* fix pipeline

* workflow fixes

* install app to emulators

* better logging

* save current version of test script

* android works. ios wip. update locks

* fix pipelines

* cr feedback

* fix android e2e test

* Split mobile e2e workflow by platform (#842)

* Replace react-native-quick-crypto with @noble/hashes (#841)

* Add tests for ethers polyfills

* Add crypto utils

* Inline crypto polyfills into ethers util

* sort and update gemfile lock

* update lock

* chore: incrementing ios build number for version 2.6.3 [github action]

* android works. ios wip. update locks

* Specify Maestro platform

* Fix Android build step in e2e workflow

* fix android

* update ios

* add concurrency

* update Podfile.lock

* fix android

* prettier

* fix

* fix android pipeline

* try job again

* fix ios

* fix android

* fix ios

* fix command

* use android runner now that path is fixed

* fix android e2e test

* fix adb

* add caching

* fix build

* speed up build

* fix

* test emulator options

* updates

* fix pipeline

* fix

* fix script and move on

* add comment

---------

Co-authored-by: Self GitHub Actions <action@github.com>

* feedback

* fixes

* ignore for now

* ignore

* fix tests

* fix ios simulator booting

* fix ios test

* shutdown after run

* fix ios test

* better timing

* increase ios timeout

* fix both flows

* fix pipeline

* combine command

* fix ios

* break up build steps for better caching

* remove cache

* fix ios and android test pipelines

* update logic

---------

Co-authored-by: Self GitHub Actions <action@github.com>
This commit is contained in:
Justin Hernandez
2025-08-06 12:41:09 -07:00
committed by GitHub
parent 091f766fd6
commit fdb932eeba
34 changed files with 1160 additions and 97 deletions

View File

@@ -69,10 +69,6 @@ app/android/build/
app/ios/build/
app/ios/Pods/
app/ios/DerivedData/
app/android/.gradle/
app/android/gradle/
app/android/gradlew
app/android/gradlew.bat
# Circuit build outputs
circuits/build/

View File

@@ -14,4 +14,4 @@ runs:
shell: bash
run: |
VERSION=$(node -p "require('${{ inputs.app_path }}/package.json').version")
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "VERSION=$VERSION" >> $GITHUB_ENV

View File

@@ -34,7 +34,6 @@ runs:
sudo locale-gen en_US.UTF-8
sudo update-locale LANG=en_US.UTF-8
- name: Setup Ruby environment
uses: ruby/setup-ruby@v1
with:
@@ -57,11 +56,11 @@ runs:
shell: bash
run: |
cd ${{ inputs.app_path }}
# Configure Yarn
corepack enable
yarn set version 4.6.0
echo "📦 Installing JavaScript dependencies with strict lock file..."
if ! yarn install --immutable; then
echo ""
@@ -77,14 +76,14 @@ runs:
echo "This ensures everyone has the exact same dependency versions."
exit 1
fi
# Run mobile-specific installation
if [[ "${{ runner.os }}" == "macOS" ]]; then
yarn install-app:mobile-deploy:ios
else
yarn install-app:mobile-deploy
fi
# Install Ruby gems with bundler (respecting cache)
echo "📦 Installing Ruby gems with strict lock file..."
if ! bundle install --jobs 4 --retry 3; then

View File

@@ -22,7 +22,7 @@ runs:
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
cache: "yarn"
cache-dependency-path: yarn.lock
- name: Install dependencies

View File

@@ -1,4 +1,4 @@
name: OpenPassport CI/CD
name: Circuits Build
on:
push:
branches:
@@ -16,7 +16,7 @@ on:
jobs:
build:
runs-on: ['self-hosted', 'selfxyz-org', 'ubuntu-22-04', '128ram']
runs-on: ["self-hosted", "selfxyz-org", "ubuntu-22-04", "128ram"]
steps:
- name: Checkout Repository

View File

@@ -1,4 +1,4 @@
name: Self Circuits CI/CD
name: Circuits CI
on:
push:
branches:
@@ -6,16 +6,16 @@ on:
- main
- openpassportv2
paths:
- 'circuits/**'
- 'common/**'
- "circuits/**"
- "common/**"
pull_request:
branches:
- dev
- main
- openpassportv2
paths:
- 'circuits/**'
- 'common/**'
- "circuits/**"
- "common/**"
jobs:
run_circuit_tests:
if: github.event.pull_request.draft == false

View File

@@ -1,4 +1,4 @@
name: General Self CI
name: Common CI
on:
pull_request:

View File

@@ -1,19 +1,19 @@
name: Self Contracts CI/CD
name: Contracts CI
on:
push:
branches:
- dev
- main
paths:
- 'contracts/**'
- 'common/**'
- "contracts/**"
- "common/**"
pull_request:
branches:
- dev
- main
paths:
- 'contracts/**'
- 'common/**'
- "contracts/**"
- "common/**"
jobs:
test_contracts:
if: github.event.pull_request.draft == false

View File

@@ -1,4 +1,4 @@
name: mobile-bundle-analysis
name: Mobile Bundle Analysis
env:
NODE_VERSION: 18

View File

@@ -1,4 +1,4 @@
name: App CI
name: Mobile CI
env:
# Build environment versions
NODE_VERSION: 18

View File

@@ -5,9 +5,9 @@ on:
types: [closed]
branches: [main, dev]
paths:
- 'app/**'
- '!app/**/*.md'
- '!app/docs/**'
- "app/**"
- "!app/**/*.md"
- "!app/docs/**"
permissions:
contents: write
@@ -22,13 +22,13 @@ jobs:
deployment_track: ${{ steps.check.outputs.deployment_track }}
version_bump: ${{ steps.check.outputs.version_bump }}
platforms: ${{ steps.check.outputs.platforms }}
steps:
- name: Check deployment conditions
id: check
run: |
echo "🔍 Checking deployment conditions..."
# Skip if PR has skip-deploy in title or body
if [[ "${{ github.event.pull_request.title }}" =~ \[skip-deploy\] ]] ||
[[ "${{ github.event.pull_request.body }}" =~ \[skip-deploy\] ]]; then
@@ -36,7 +36,7 @@ jobs:
echo "⏭️ Skipping deployment due to [skip-deploy] flag"
exit 0
fi
# Determine deployment track based on target branch
if [[ "${{ github.base_ref }}" == "main" ]]; then
echo "deployment_track=production" >> $GITHUB_OUTPUT
@@ -45,7 +45,7 @@ jobs:
echo "deployment_track=internal" >> $GITHUB_OUTPUT
echo "🧪 Deployment track: internal testing"
fi
# Determine version bump from PR labels
labels="${{ join(github.event.pull_request.labels.*.name, ',') }}"
if [[ "$labels" =~ version:major ]]; then
@@ -61,11 +61,11 @@ jobs:
echo "version_bump=build" >> $GITHUB_OUTPUT
echo "📈 Version bump: build only"
fi
# Always deploy both platforms for now (can be enhanced later)
echo 'platforms=["ios", "android"]' >> $GITHUB_OUTPUT
echo "should_deploy=true" >> $GITHUB_OUTPUT
- name: Log deployment info
if: steps.check.outputs.should_deploy == 'true'
run: |
@@ -106,4 +106,4 @@ jobs:
- name: Notify skip
run: |
echo "📱 Mobile deployment was skipped for this PR"
echo "To deploy manually, use the 'Run workflow' button on the Mobile App Deployments workflow"
echo "To deploy manually, use the 'Run workflow' button on the Mobile App Deployments workflow"

View File

@@ -1,4 +1,4 @@
name: Mobile App Deployments
name: Mobile Deploy
env:
# Build environment versions
@@ -9,11 +9,11 @@ env:
ANDROID_NDK_VERSION: 27.0.11718014
# Cache versioning - increment these to bust caches when needed
GH_CACHE_VERSION: v1 # Global cache version
GH_YARN_CACHE_VERSION: v1 # Yarn-specific cache version
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
GH_PODS_CACHE_VERSION: v1 # CocoaPods cache version
GH_GRADLE_CACHE_VERSION: v1 # Gradle cache version
GH_CACHE_VERSION: v1 # Global cache version
GH_YARN_CACHE_VERSION: v1 # Yarn-specific cache version
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
GH_PODS_CACHE_VERSION: v1 # CocoaPods cache version
GH_GRADLE_CACHE_VERSION: v1 # Gradle cache version
# Path configuration
WORKSPACE: ${{ github.workspace }}

275
.github/workflows/mobile-e2e.yml vendored Normal file
View File

@@ -0,0 +1,275 @@
name: Mobile E2E
env:
# Build environment versions
NODE_VERSION: 18
JAVA_VERSION: 17
ANDROID_API_LEVEL: 33
ANDROID_NDK_VERSION: 27.0.11718014
# Performance optimizations
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.caching=true
CI: true
# Disable Maestro analytics in CI
MAESTRO_CLI_NO_ANALYTICS: true
MAESTRO_VERSION: 1.41.0
on:
push:
branches: [main, release/**]
paths:
- "app/**"
- ".github/workflows/mobile-e2e.yml"
pull_request:
paths:
- "app/**"
- ".github/workflows/mobile-e2e.yml"
jobs:
e2e-android:
# TODO: The Android E2E test job is temporarily disabled due to a recurring
# Maestro driver timeout issue in the CI environment. The emulator becomes
# unresponsive, preventing Maestro from connecting. This needs further
# investigation, but has been disabled to unblock the pipeline.
# To test locally, run `./scripts/test-e2e-local.sh android --workflow-match`
if: false
concurrency:
group: ${{ github.workflow }}-android-${{ github.ref }}
cancel-in-progress: true
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate
- name: Cache Yarn dependencies
uses: actions/cache@v4
with:
path: .yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install --immutable --silent
- name: Cache Maestro
id: cache-maestro
uses: actions/cache@v4
with:
path: ~/.maestro
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
- name: Install Maestro
if: steps.cache-maestro.outputs.cache-hit != 'true'
run: curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Add Maestro to path
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Setup Java environment
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
accept-android-sdk-licenses: true
- name: Cache NDK
uses: actions/cache@v4
with:
path: ${{ env.ANDROID_HOME }}/ndk/${{ env.ANDROID_NDK_VERSION }}
key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }}
- name: Install NDK
run: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"
- name: Build dependencies (outside emulator)
run: |
echo "Building dependencies..."
yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; }
echo "✅ Dependencies built successfully"
- name: Cache Android build
uses: actions/cache@v4
with:
path: |
app/android/app/build
app/android/.gradle
key: ${{ runner.os }}-android-build-${{ hashFiles('app/android/**/*.gradle*', 'app/android/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-android-build-
- name: Build Android APK
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.ANDROID_API_LEVEL }}
arch: x86_64
target: google_apis
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 8192 -cores 4 -accel on
disable-animations: true
script: |
echo "Building Android APK..."
chmod +x app/android/gradlew
(cd app/android && ./gradlew assembleRelease --quiet --parallel --build-cache --no-configuration-cache) || { echo "❌ Android build failed"; exit 1; }
echo "✅ Android build succeeded"
- name: Install and Test on Android
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.ANDROID_API_LEVEL }}
arch: x86_64
target: google_apis
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 8192 -cores 4 -accel on
disable-animations: true
script: |
echo "Installing app on emulator..."
APK_PATH="app/android/app/build/outputs/apk/release/app-release.apk"
[ -f "$APK_PATH" ] || { echo "❌ APK not found at $APK_PATH"; exit 1; }
adb install -r "$APK_PATH" || { echo "❌ App installation failed"; exit 1; }
echo "✅ App installed successfully"
echo "⏰ Giving the emulator a moment to settle..."
sleep 5
echo "🎭 Running Maestro tests..."
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000
maestro test tests/e2e/launch.android.flow.yaml --format junit --output app/maestro-results.xml
env:
E2E_BUILD: "true"
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-results-android
path: app/maestro-results.xml
if-no-files-found: warn
e2e-ios:
timeout-minutes: 45
runs-on: macos-latest
concurrency:
group: ${{ github.workflow }}-ios-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate
- name: Cache Yarn dependencies
uses: actions/cache@v4
with:
path: .yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install --immutable --silent
- name: Cache Maestro
id: cache-maestro
uses: actions/cache@v4
with:
path: ~/.maestro
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
- name: Install Maestro
if: steps.cache-maestro.outputs.cache-hit != 'true'
run: curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Add Maestro to path
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Cache Pods
uses: actions/cache@v4
with:
path: |
app/ios/Pods
~/Library/Caches/CocoaPods
key: ${{ runner.os }}-pods-${{ hashFiles('app/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Cache Xcode build
uses: actions/cache@v4
with:
path: app/ios/build
key: ${{ runner.os }}-xcode-${{ hashFiles('app/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-xcode-
- name: Build dependencies (outside main flow)
run: |
echo "Building dependencies..."
yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; }
echo "✅ Dependencies built successfully"
- name: Install iOS dependencies
run: |
echo "Installing iOS dependencies..."
(cd app/ios && pod install --silent) || { echo "❌ Pod install failed"; exit 1; }
echo "✅ Pods installed successfully"
- name: Setup iOS Simulator
run: |
echo "Setting up iOS Simulator..."
echo "Available simulators:"
xcrun simctl list devices | grep "iPhone 15"
xcrun simctl boot "iPhone 15" || true
xcrun simctl bootstatus "iPhone 15" -b
echo "Simulator status:"
xcrun simctl list devices | grep "iPhone 15"
- name: Build iOS App
run: |
echo "Building iOS app..."
xcodebuild -workspace app/ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration Release -sdk iphonesimulator -derivedDataPath app/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet || { echo "❌ iOS build failed"; exit 1; }
echo "✅ iOS build succeeded"
- name: Install and Test on iOS
run: |
echo "Installing app on simulator..."
APP_PATH=$(find app/ios/build/Build/Products/Release-iphonesimulator -name "*.app" | head -1)
[ -z "$APP_PATH" ] && { echo "❌ Could not find built iOS app"; exit 1; }
echo "Found app at: $APP_PATH"
echo "🔍 Determining app bundle ID from built app..."
IOS_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP_PATH/Info.plist")
[ -z "$IOS_BUNDLE_ID" ] && { echo "❌ Could not determine bundle ID from $APP_PATH/Info.plist"; exit 1; }
echo "✅ App Bundle ID: $IOS_BUNDLE_ID"
echo "Removing any existing app installation..."
xcrun simctl uninstall "iPhone 15" "$IOS_BUNDLE_ID" 2>/dev/null || true
echo "Installing app..."
xcrun simctl install "iPhone 15" "$APP_PATH"
if [ $? -ne 0 ]; then
echo "❌ iOS app installation failed"
exit 1
fi
echo "Verifying app installation..."
if xcrun simctl listapps "iPhone 15" | grep -q "$IOS_BUNDLE_ID"; then
echo "✅ App successfully installed"
else
echo "❌ App installation verification failed"
exit 1
fi
echo "🚀 Testing app launch capability..."
xcrun simctl launch "iPhone 15" "$IOS_BUNDLE_ID" || {
echo "⚠️ Direct app launch test failed - this might be expected."
}
echo "⏰ Checking simulator readiness..."
sleep 10
xcrun simctl listapps "iPhone 15" > /dev/null || sleep 5
echo "Running Maestro tests..."
echo "Starting test execution..."
maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml || {
echo "Maestro test failed, but continuing to upload results..."
exit 1
}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-results-ios
path: app/maestro-results.xml
if-no-files-found: warn

View File

@@ -1,13 +1,13 @@
name: Publish to npm
name: NPM Publish
on:
push:
branches:
- dev
paths:
- 'sdk/core/package.json'
- 'sdk/qrcode/package.json'
- 'common/package.json'
- "sdk/core/package.json"
- "sdk/qrcode/package.json"
- "common/package.json"
workflow_dispatch:
jobs:
@@ -52,8 +52,8 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
node-version: "18"
registry-url: "https://registry.npmjs.org"
- name: Install Dependencies
uses: ./.github/actions/yarn-install
@@ -82,8 +82,8 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
node-version: "18"
registry-url: "https://registry.npmjs.org"
- name: Install Dependencies
uses: ./.github/actions/yarn-install
@@ -111,8 +111,8 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
node-version: "18"
registry-url: "https://registry.npmjs.org"
- uses: actions/checkout@v4
- name: Install Dependencies
uses: ./.github/actions/yarn-install

View File

@@ -1,6 +1,7 @@
# AGENTS Instructions
This repository is a Yarn v4 monorepo with several workspaces:
- `app` mobile app (@selfxyz/mobile-app)
- `circuits` zk-SNARK circuits (@selfxyz/circuits)
- `common` shared utilities (@selfxyz/common)
@@ -11,9 +12,11 @@ This repository is a Yarn v4 monorepo with several workspaces:
## Workflow
### Setup
- Run `yarn install` once before running any other commands. This installs root dependencies and sets up husky hooks.
### Commit Checks
Before committing, run the following commands:
```bash
@@ -31,19 +34,23 @@ yarn types
```
### Tests
- Run unit tests where available:
- `yarn workspace @selfxyz/common test`
- `yarn workspace @selfxyz/circuits test` # may fail if OpenSSL algorithms are missing
- `yarn workspace @selfxyz/circuits test` # may fail if OpenSSL algorithms are missing
- `yarn workspace @selfxyz/mobile-app test`
- Tests for `@selfxyz/contracts` are currently disabled in CI and may be skipped.
### Formatting
- Use Prettier configuration from `.prettierrc` files.
- Follow `.editorconfig` for line endings and indentation.
### Commit Guidelines
- Write short, imperative commit messages (e.g. `Fix address validation`).
- The pull request body should summarize the changes and mention test results.
## Scope
These instructions apply to the entire repository unless overridden by a nested `AGENTS.md`.

View File

@@ -7,6 +7,7 @@ By scanning the NFC chip in their ID document, users can prove their validity wh
Under the hood, Self uses zk-SNARKs to make sure personal data is redacted, but the document is verified.
Use cases unlocked include:
- **Airdrop protection**: Protect a token distribution from bots
- **Social media**: Add humanity checks to user's profiles
- **Quadratic funding**: Prevent farmers from skewing rewards
@@ -27,6 +28,7 @@ Checkout our [coverage map here](http://map.self.xyz/).
#### What can I request/prove with Self?
When a country issues a passport or a compliant ID document, they sign datagroups that include at least:
- First and last name
- Nationality
- Date of birth
@@ -59,7 +61,7 @@ The International Civil Aviation Organization (ICAO) is a specialized agency of
- Allow DeFi protocols to check if the nationality of a user is included in a set of forbidden states.
- Gate an adult content website to a specific age.
- Create a petition system or a survey portal.
- Passport Wallet: use [active authentication](https://en.wikipedia.org/wiki/Biometric_passport#:~:text=Active%20Authentication%20(AA),Using%20AA%20is%20optional.) to build a wallet, a multisig or a recovery module using passport signatures
- Passport Wallet: use [active authentication](<https://en.wikipedia.org/wiki/Biometric_passport#:~:text=Active%20Authentication%20(AA),Using%20AA%20is%20optional.>) to build a wallet, a multisig or a recovery module using passport signatures
We provide bounties for new and interesting applications using Self.

View File

@@ -4,7 +4,7 @@ codecov:
coverage:
precision: 2
round: down
range: '80...100'
range: "80...100"
status:
project:
default:
@@ -18,36 +18,36 @@ coverage:
flags:
screens:
paths:
- 'src/screens/'
- "src/screens/"
stores:
paths:
- 'src/stores/'
- "src/stores/"
providers:
paths:
- 'src/providers/'
- "src/providers/"
components:
paths:
- 'src/components/'
- "src/components/"
utils:
paths:
- 'src/utils/'
- "src/utils/"
hooks:
paths:
- 'src/hooks/'
- "src/hooks/"
navigation:
paths:
- 'src/navigation/'
- "src/navigation/"
layouts:
paths:
- 'src/layouts/'
- "src/layouts/"
ignore:
- 'src/mocks/**'
- 'src/types/**'
- 'src/**/*.d.ts'
- 'src/**/index.{ts,tsx}'
- 'src/**/*.test.{ts,tsx}'
- 'src/**/*.spec.{ts,tsx}'
- 'src/**/__tests__/**'
- 'src/**/__mocks__/**'
- 'tests/**'
- "src/mocks/**"
- "src/types/**"
- "src/**/*.d.ts"
- "src/**/index.{ts,tsx}"
- "src/**/*.test.{ts,tsx}"
- "src/**/*.spec.{ts,tsx}"
- "src/**/__tests__/**"
- "src/**/__mocks__/**"
- "tests/**"

View File

@@ -4,13 +4,13 @@ on:
push:
branches: [main, dev]
paths:
- 'app/**'
- '.github/workflows/test-coverage.yml'
- "app/**"
- ".github/workflows/test-coverage.yml"
pull_request:
branches: [main, dev]
paths:
- 'app/**'
- '.github/workflows/test-coverage.yml'
- "app/**"
- ".github/workflows/test-coverage.yml"
jobs:
test:
@@ -22,8 +22,8 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'yarn'
node-version: "22"
cache: "yarn"
- name: Install dependencies
run: |

3
app/.gitignore vendored
View File

@@ -92,3 +92,6 @@ yarn-error.log
# web app
.tamagui/*
# Maestro
maestro-results.xml

View File

@@ -7,5 +7,13 @@
"tabWidth": 2,
"useTabs": false,
"semi": true,
"endOfLine": "auto"
"endOfLine": "auto",
"overrides": [
{
"files": ["*.yml", "*.yaml"],
"options": {
"singleQuote": false
}
}
]
}

View File

@@ -1,2 +1,2 @@
enableScripts: true
checksumBehavior: 'update'
checksumBehavior: "update"

View File

@@ -25,7 +25,7 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1141.0)
aws-partitions (1.1142.0)
aws-sdk-core (3.229.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -37,7 +37,7 @@ GEM
aws-sdk-kms (1.110.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.196.0)
aws-sdk-s3 (1.196.1)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)

View File

@@ -341,6 +341,25 @@ bundle exec fastlane ios internal_test test_mode:true
bundle exec fastlane android internal_test test_mode:true
```
### Maestro end-to-end tests
Install the Maestro CLI locally using curl or Homebrew:
```bash
curl -Ls https://get.maestro.mobile.dev | bash
# or
brew install maestro
```
Then build the app and run the flow:
```bash
yarn test:e2e:android # Android
yarn test:e2e:ios # iOS
```
The flow definition for Android is in [`tests/e2e/launch.android.flow.yaml`](tests/e2e/launch.android.flow.yaml) and for iOS is in [`tests/e2e/launch.ios.flow.yaml`](tests/e2e/launch.ios.flow.yaml).
## FAQ
If you get something like this:

View File

@@ -130,7 +130,11 @@ android {
arguments += "-DANDROID_STL=c++_shared"
}
ndk {
abiFilters += "arm64-v8a"
if (System.env.E2E_BUILD == "true") {
abiFilters "x86_64"
} else {
abiFilters "arm64-v8a", "x86_64"
}
}
}
}
@@ -147,6 +151,12 @@ android {
storePassword MYAPP_UPLOAD_STORE_PASSWORD
keyAlias MYAPP_UPLOAD_KEY_ALIAS
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
} else {
// Fallback to debug signing for CI/local testing
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
}

View File

@@ -9,9 +9,12 @@
import Foundation
import React
#if !E2E_TESTING
import NFCPassportReader
#endif
import Security
#if !E2E_TESTING
@available(iOS 13, macOS 10.15, *)
extension CertificateType {
func stringValue() -> String {
@@ -23,6 +26,7 @@ extension CertificateType {
}
}
}
#endif
// Helper function to map the keys of a dictionary
extension Dictionary {
@@ -31,6 +35,7 @@ extension Dictionary {
}
}
#if !E2E_TESTING
@available(iOS 15, *)
@objc(PassportReader)
class PassportReader: NSObject {
@@ -416,3 +421,38 @@ func serializePublicKey(_ publicKey: SecKey) -> String? {
return true
}
}
#else
// E2E Testing stub implementation
@available(iOS 15, *)
@objc(PassportReader)
class PassportReader: NSObject {
override init() {
super.init()
}
@objc(configure:enableDebugLogs:)
func configure(token: String, enableDebugLogs: Bool) {
// No-op for E2E testing
}
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:resolve:reject:)
func scanPassport(
_ passportNumber: String,
dateOfBirth: String,
dateOfExpiry: String,
canNumber: String,
useCan: NSNumber,
skipPACE: NSNumber,
skipCA: NSNumber,
extendedMode: NSNumber,
usePacePolling: NSNumber,
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
reject("E2E_TESTING", "NFC scanning not available in E2E testing mode", nil)
}
@objc
static func requiresMainQueueSetup() -> Bool {
return true
}
}
#endif

View File

@@ -27,7 +27,10 @@ target "Self" do
config = use_native_modules!
use_frameworks!
pod "NFCPassportReader", git: "https://github.com/seshanthS/NFCPassportReader", commit: "74098a5e29c23b3f5a58dc14a336556fa89c0ad6"
# Skip NFCPassportReader for e2e testing to avoid build issues
unless ENV["E2E_TESTING"] == "1"
pod "NFCPassportReader", git: "https://github.com/seshanthS/NFCPassportReader", commit: "74098a5e29c23b3f5a58dc14a336556fa89c0ad6"
end
pod "QKMRZScanner"
pod "lottie-ios"
@@ -63,6 +66,7 @@ target "Self" do
end
end
end
target.build_configurations.each do |config|
config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = "15.0"
config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"] ||= ["$(inherited)", "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"]
@@ -79,15 +83,18 @@ target "Self" do
system(command)
end
framework_paths = [
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64/OpenSSL.framework/OpenSSL",
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64_x86_64-maccatalyst/OpenSSL.framework/OpenSSL",
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64_x86_64-simulator/OpenSSL.framework/OpenSSL",
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/macos-arm64_x86_64/OpenSSL.framework/OpenSSL",
]
# Only strip OpenSSL bitcode if NFCPassportReader is included (not in e2e testing)
unless ENV["E2E_TESTING"] == "1"
framework_paths = [
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64/OpenSSL.framework/OpenSSL",
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64_x86_64-maccatalyst/OpenSSL.framework/OpenSSL",
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/ios-arm64_x86_64-simulator/OpenSSL.framework/OpenSSL",
"Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework/macos-arm64_x86_64/OpenSSL.framework/OpenSSL",
]
framework_paths.each do |framework_relative_path|
strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path)
framework_paths.each do |framework_relative_path|
strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path)
end
end
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
@@ -136,5 +143,25 @@ target "Self" do
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
end
end
# Add E2E_TESTING compilation condition for main app target when environment variable is set
if ENV["E2E_TESTING"] == "1"
# Find Self.xcodeproj and add E2E_TESTING compilation condition
self_project_path = File.join(installer.sandbox.project_path, "../Self.xcodeproj")
if File.exist?(self_project_path)
project = Xcodeproj::Project.open(self_project_path)
project.targets.each do |target|
if target.name == "Self"
target.build_configurations.each do |config|
existing_conditions = config.build_settings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] || ""
unless existing_conditions.to_s.include?("E2E_TESTING")
config.build_settings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = (existing_conditions.to_s + " E2E_TESTING").strip
end
end
end
end
project.save
end
end
end
end

View File

@@ -2284,6 +2284,6 @@ SPEC CHECKSUMS:
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
Yoga: b05994d1933f507b0a28ceaa4fdb968dc18da178
PODFILE CHECKSUM: 558a8b95f1ca0bd657ecdbe22eb0b6972605ad2b
PODFILE CHECKSUM: 8ed8bfe711f629f0a3f4d4fdacf04034737d609b
COCOAPODS: 1.16.2

View File

@@ -55,6 +55,8 @@
"test:build": "yarn build:deps && yarn web:build && yarn types && yarn analyze:bundle:ios",
"test:coverage": "jest --coverage --passWithNoTests",
"test:coverage:ci": "jest --coverage --passWithNoTests --ci --coverageReporters=lcov --coverageReporters=text --coverageReporters=json",
"test:e2e:android": "cd android && ./gradlew assembleDebug && cd .. && maestro test tests/e2e/launch.android.flow.yaml",
"test:e2e:ios": "xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build && maestro test tests/e2e/launch.ios.flow.yaml",
"test:fastlane": "bundle exec ruby -Itest fastlane/test/helpers_test.rb",
"test:tree-shaking": "node ./scripts/test-tree-shaking.cjs",
"test:web-build": "jest tests/web-build-render.test.ts --testTimeout=180000",

659
app/scripts/test-e2e-local.sh Executable file
View File

@@ -0,0 +1,659 @@
#!/bin/bash
# Unified Local E2E Testing Script
# Run this from the app directory
set -e
PLATFORM=${1:-}
EMULATOR_PID=""
# Colors for better output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_usage() {
echo "🎭 Local E2E Testing"
echo "Usage: $0 [ios|android] [--workflow-match]"
echo ""
echo "Examples:"
echo " $0 ios - Run iOS e2e tests locally"
echo " $0 android - Run Android e2e tests locally"
echo " $0 android --workflow-match - Run Android tests matching GitHub Actions workflow"
echo ""
echo "Prerequisites:"
echo " iOS: Xcode, iOS Simulator, CocoaPods"
echo " Android: Android SDK, running emulator"
echo ""
echo "Workflow Match Mode:"
echo " --workflow-match - Use Release builds and exact workflow steps"
echo " (No Metro dependency, matches CI environment)"
}
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Check if we're in the right directory (app directory)
check_directory() {
if [ ! -f "package.json" ]; then
log_error "Please run this from the app directory (where package.json exists)"
echo "Current directory: $(pwd)"
echo "Expected: /path/to/your/project/app"
exit 1
fi
}
# Check if Maestro is installed and install if needed
setup_maestro() {
if ! command -v maestro &> /dev/null; then
if [ -f "$HOME/.maestro/bin/maestro" ]; then
log_info "📦 Maestro found in ~/.maestro/bin, adding to PATH..."
export PATH="$HOME/.maestro/bin:$PATH"
else
log_info "📦 Installing Maestro..."
curl -Ls "https://get.maestro.mobile.dev" | bash
export PATH="$HOME/.maestro/bin:$PATH"
log_success "Maestro installed successfully"
fi
else
log_success "Maestro already available in PATH"
fi
}
# Check if Metro is running (required for debug builds)
check_metro_running() {
# Skip Metro check if in workflow match mode (Release builds don't need Metro)
if [ "$WORKFLOW_MATCH" = "true" ]; then
log_info "🔍 Skipping Metro check (Release builds don't need Metro)"
return
fi
log_info "🔍 Checking if Metro server is running..."
# Check if Metro is running on port 8081
if ! curl -f -s http://localhost:8081/status > /dev/null 2>&1; then
log_error "Metro server is not running!"
echo ""
echo "React Native debug builds require Metro to serve the JavaScript bundle."
echo "Please start Metro in another terminal before running e2e tests:"
echo ""
echo " ${BLUE}cd $(pwd)${NC}"
echo " ${BLUE}yarn start${NC}"
echo ""
echo "Wait for Metro to show 'Metro waiting on exp://localhost:8081' then re-run this script."
echo ""
echo "Or use --workflow-match to use Release builds (no Metro needed):"
echo " ${BLUE}$0 $PLATFORM --workflow-match${NC}"
exit 1
else
log_success "Metro server is running on http://localhost:8081"
fi
}
# Build dependencies (shared by both platforms)
build_dependencies() {
log_info "🔨 Building dependencies..."
if ! yarn build:deps; then
log_error "Dependency build failed"
exit 1
fi
}
# Run Maestro tests (shared by both platforms)
run_maestro_tests() {
log_info "🎭 Running Maestro tests..."
echo "Starting test execution..."
# Use platform-specific flow files
if [ "$PLATFORM" = "ios" ]; then
FLOW_FILE="tests/e2e/launch.ios.flow.yaml"
else
FLOW_FILE="tests/e2e/launch.android.flow.yaml"
fi
# Set a longer timeout for the driver to start, especially for the first run
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 # 180 seconds (3 minutes) in ms
# Attempt to run Maestro, capturing output to check for a specific error
MAESTRO_OUTPUT_FILE=$(mktemp)
if maestro test "$FLOW_FILE" --format junit --output maestro-results.xml > "$MAESTRO_OUTPUT_FILE" 2>&1; then
log_success "🎉 Maestro tests passed on the first attempt!"
cat "$MAESTRO_OUTPUT_FILE"
rm "$MAESTRO_OUTPUT_FILE"
return 0
else
# First attempt failed, check for known timeout errors
cat "$MAESTRO_OUTPUT_FILE"
if grep -q "MaestroDriverStartupException\|IOSDriverTimeoutException" "$MAESTRO_OUTPUT_FILE"; then
log_warning "Maestro driver failed to start. Retrying in 30 seconds..."
sleep 30
# Second attempt
log_info "🎭 Retrying Maestro tests..."
if maestro test "$FLOW_FILE" --format junit --output maestro-results.xml; then
log_success "🎉 Maestro tests passed on the second attempt!"
rm "$MAESTRO_OUTPUT_FILE"
return 0
else
log_error "Maestro tests failed on the second attempt."
rm "$MAESTRO_OUTPUT_FILE"
return 1
fi
else
# Failed for a different reason, so don't retry
log_error "Maestro tests failed for a reason other than driver timeout."
rm "$MAESTRO_OUTPUT_FILE"
return 1
fi
fi
}
shutdown_all_simulators() {
log_info "🔌 Shutting down all running iOS simulators..."
xcrun simctl shutdown all
log_success "All simulators shut down"
}
# iOS-specific functions
setup_ios_environment() {
# Check if Xcode is available
if ! command -v xcrun &> /dev/null; then
log_error "Xcode not found. Please install Xcode and iOS Simulator"
exit 1
fi
log_info "🍎 Setting up iOS environment..."
cd ios
echo "Installing CocoaPods dependencies with e2e configuration..."
# Set environment variable for e2e testing to enable OpenSSL fixes
export E2E_TESTING=1
pod install
cd ..
}
setup_ios_simulator() {
log_info "📱 Setting up iOS Simulator..."
# Get available iOS simulators
echo "Available simulators:"
xcrun simctl list devices
# Find the first available iPhone simulator (prefer booted ones, then shutdown ones)
AVAILABLE_SIMULATOR=$(xcrun simctl list devices | grep "iPhone" | grep "(Booted)" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
if [ -z "$AVAILABLE_SIMULATOR" ]; then
# Try to find any available iPhone simulator that's shutdown
AVAILABLE_SIMULATOR=$(xcrun simctl list devices | grep "iPhone" | grep "(Shutdown)" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
fi
if [ -z "$AVAILABLE_SIMULATOR" ]; then
# Try to find any available simulator
AVAILABLE_SIMULATOR=$(xcrun simctl list devices | grep "(Booted)" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
fi
if [ -z "$AVAILABLE_SIMULATOR" ]; then
# Last resort - any shutdown simulator
AVAILABLE_SIMULATOR=$(xcrun simctl list devices | grep "(Shutdown)" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
fi
if [ -z "$AVAILABLE_SIMULATOR" ]; then
log_error "No available simulators found. Please create a simulator in Xcode."
exit 1
fi
# Get the simulator name for display
SIMULATOR_NAME=$(xcrun simctl list devices | grep "$AVAILABLE_SIMULATOR" | sed -E 's/^[[:space:]]*([^(]+).*/\1/' | xargs)
log_info "Using simulator: $SIMULATOR_NAME ($AVAILABLE_SIMULATOR)"
# Boot the simulator and ensure the Simulator app is open
log_info "Booting $SIMULATOR_NAME simulator..."
# This can fail if the device is already booted. The `|| true` handles this gracefully.
# Our shutdown command should prevent this, but we keep it for robustness.
xcrun simctl boot "$AVAILABLE_SIMULATOR" || true
log_info "Opening Simulator app to ensure it is visible..."
open -a Simulator
log_info "Waiting for simulator to fully boot..."
xcrun simctl bootstatus "$AVAILABLE_SIMULATOR" -b
# Store the simulator ID for later use
export IOS_SIMULATOR_ID="$AVAILABLE_SIMULATOR"
export IOS_SIMULATOR_NAME="$SIMULATOR_NAME"
echo "Simulator status:"
xcrun simctl list devices | grep "$AVAILABLE_SIMULATOR"
}
build_ios_app() {
log_info "🔨 Building iOS app..."
# Set environment variable for e2e testing to enable OpenSSL fixes
export E2E_TESTING=1
# Set build configuration based on workflow match
if [ "$WORKFLOW_MATCH" = "true" ]; then
log_info "Using Release configuration for workflow match"
BUILD_CONFIG="Release"
else
log_info "Using Debug configuration for local development"
BUILD_CONFIG="Debug"
fi
if ! xcodebuild -workspace ios/OpenPassport.xcworkspace -scheme OpenPassport -configuration "$BUILD_CONFIG" -sdk iphonesimulator -derivedDataPath ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets SWIFT_ACTIVE_COMPILATION_CONDITIONS="E2E_TESTING"; then
log_error "iOS build failed"
exit 1
fi
log_success "iOS build succeeded"
}
install_ios_app() {
log_info "📦 Installing app on simulator..."
APP_PATH=$(find "ios/build/Build/Products/$BUILD_CONFIG-iphonesimulator" -name "*.app" | head -1)
if [ -z "$APP_PATH" ]; then
log_error "Could not find built iOS app in ios/build/Build/Products/$BUILD_CONFIG-iphonesimulator"
exit 1
fi
echo "Found app at: $APP_PATH"
# Dynamically determine the bundle ID from the built app
log_info "🔍 Determining app bundle ID from built app..."
IOS_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP_PATH/Info.plist")
if [ -z "$IOS_BUNDLE_ID" ]; then
log_error "Could not determine bundle ID from $APP_PATH/Info.plist"
exit 1
fi
export IOS_BUNDLE_ID
log_success "App Bundle ID: $IOS_BUNDLE_ID"
# Use the dynamic simulator ID
SIMULATOR_ID="${IOS_SIMULATOR_ID:-iPhone 15}"
log_info "Installing on simulator: $SIMULATOR_ID"
# Uninstall any existing version first
echo "Removing any existing app installation..."
xcrun simctl uninstall "$SIMULATOR_ID" "$IOS_BUNDLE_ID" 2>/dev/null || true
# Install the app
echo "Installing app..."
if ! xcrun simctl install "$SIMULATOR_ID" "$APP_PATH"; then
log_error "iOS app installation failed"
exit 1
fi
# Verify the app is installed
echo "Verifying app installation..."
echo "All installed apps on simulator:"
xcrun simctl listapps "$SIMULATOR_ID"
echo "Checking for exact bundle ID: $IOS_BUNDLE_ID"
if xcrun simctl listapps "$SIMULATOR_ID" | grep -q "$IOS_BUNDLE_ID"; then
log_success "App successfully installed"
else
log_error "App installation verification failed"
exit 1
fi
# Test if the app can be launched directly
log_info "🚀 Testing app launch capability..."
if ! xcrun simctl launch "$SIMULATOR_ID" "$IOS_BUNDLE_ID"; then
log_warning "Direct app launch test failed - this might be expected if the app has launch conditions, but it could also indicate a problem."
fi
}
# Android-specific functions
setup_android_environment() {
# Check if Android SDK is configured
if [ -z "$ANDROID_HOME" ]; then
log_error "ANDROID_HOME environment variable is not set."
echo "Please set ANDROID_HOME to your Android SDK directory."
exit 1
fi
# Define and export full paths to tools for robustness
export ADB_CMD="$ANDROID_HOME/platform-tools/adb"
export EMULATOR_CMD="$ANDROID_HOME/emulator/emulator"
if [ ! -f "$ADB_CMD" ]; then
log_error "adb not found at $ADB_CMD"
echo "Please ensure your ANDROID_HOME is set correctly."
exit 1
fi
# Check if emulator is running
log_info "📱 Checking for Android emulator..."
# Set shorter wait time for emulator shutdown to reduce logging
export ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL=5
RUNNING_EMULATOR=$($ADB_CMD devices | grep emulator | head -1 | cut -f1)
if [ -z "$RUNNING_EMULATOR" ]; then
log_info "No Android emulator running. Attempting to start one..."
# Check if emulator command is available
if [ ! -f "$EMULATOR_CMD" ]; then
log_error "emulator command not found at $EMULATOR_CMD"
echo "Please ensure your ANDROID_HOME is set correctly."
exit 1
fi
# Get available AVDs
log_info "Finding available Android Virtual Devices..."
AVAILABLE_AVDS=$($EMULATOR_CMD -list-avds)
if [ -z "$AVAILABLE_AVDS" ]; then
log_error "No Android Virtual Devices (AVDs) found."
echo "Please create an AVD in Android Studio:"
echo " 1. Open Android Studio"
echo " 2. Go to Tools > Device Manager"
echo " 3. Create Virtual Device"
exit 1
fi
# Use the first available AVD
FIRST_AVD=$(echo "$AVAILABLE_AVDS" | head -1)
log_info "Using emulator: $FIRST_AVD"
# Start the emulator in background
log_info "Starting emulator (this may take a minute)..."
"$EMULATOR_CMD" -avd "$FIRST_AVD" -no-snapshot-load >/dev/null 2>&1 &
EMULATOR_PID=$!
# Wait for emulator to start
log_info "Waiting for emulator to boot..."
for i in {1..60}; do
if "$ADB_CMD" devices | grep -q emulator; then
RUNNING_EMULATOR=$("$ADB_CMD" devices | grep emulator | head -1 | cut -f1)
log_success "Emulator started: $RUNNING_EMULATOR"
break
fi
echo -n "."
sleep 2
done
if [ -z "$RUNNING_EMULATOR" ]; then
log_error "Emulator failed to start within 2 minutes"
echo "You can try starting it manually:"
echo " $EMULATOR_CMD -avd $FIRST_AVD"
exit 1
fi
# Wait for emulator to be fully booted
log_info "Waiting for emulator to be fully booted..."
for i in {1..30}; do
if "$ADB_CMD" -s "$RUNNING_EMULATOR" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
log_success "Emulator fully booted and ready"
break
fi
echo -n "."
sleep 2
done
else
log_success "Android emulator already running: $RUNNING_EMULATOR"
# Ensure the running emulator is fully booted
log_info "Checking if emulator is fully booted..."
if ! "$ADB_CMD" -s "$RUNNING_EMULATOR" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
log_warning "Emulator is running but not fully booted, waiting..."
for i in {1..15}; do
if "$ADB_CMD" -s "$RUNNING_EMULATOR" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
log_success "Emulator is now fully booted"
break
fi
echo -n "."
sleep 2
done
else
log_success "Emulator is fully booted and ready"
fi
fi
# Store the emulator device ID for later use
export ANDROID_EMULATOR_ID="$RUNNING_EMULATOR"
log_success "Android emulator ready:"
"$ADB_CMD" devices
}
build_android_app() {
log_info "🔨 Building Android APK..."
# Note: Using Release builds to avoid Metro dependency in CI
# Debug builds require Metro server, Release builds have JS bundled
# Run the build inside the android directory so gradlew is available
echo "Current working directory: $(pwd)"
echo "Checking if gradlew exists:"
ls -la android/gradlew || echo "gradlew not found in android/"
cd android
if ! ./gradlew assembleRelease --quiet; then
log_error "Android build failed"
exit 1
fi
log_success "Android build succeeded"
cd ..
}
install_android_app() {
log_info "📦 Installing app on emulator..."
# Check if APK was built successfully (matching workflow)
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
log_info "Looking for APK at: $APK_PATH"
if [ ! -f "$APK_PATH" ]; then
log_error "APK not found at expected location"
echo "Available files in build directory:"
find android/app/build -name "*.apk" 2>/dev/null || echo "No APK files found"
exit 1
fi
echo "Found APK at: $APK_PATH"
# Use the dynamic emulator ID
EMULATOR_ID="${ANDROID_EMULATOR_ID:-emulator-5554}"
log_info "Installing on emulator: $EMULATOR_ID"
# Dynamically find the latest 'aapt' tool path and determine package name
# First try 'aapt' (classic tool) for package name extraction, then fall back to 'aapt2' with supported commands
AAPT_PATH=$(find "$ANDROID_HOME/build-tools" -type f -name "aapt" | sort -r | head -n 1)
if [ -n "$AAPT_PATH" ]; then
log_info "Using aapt to get package name from $APK_PATH..."
ACTUAL_PACKAGE=$("$AAPT_PATH" dump badging "$APK_PATH" 2>/dev/null | grep "package:" | sed -E "s/.*name='([^']+)'.*/\1/" | head -1)
else
log_warning "aapt not found, trying aapt2..."
AAPT2_PATH=$(find "$ANDROID_HOME/build-tools" -type f -name "aapt2" | sort -r | head -n 1)
if [ -n "$AAPT2_PATH" ]; then
log_info "Found aapt2 at: $AAPT2_PATH"
# aapt2 doesn't support 'dump packagename', so we'll use 'dump xmltree' to parse the manifest
ACTUAL_PACKAGE=$("$AAPT2_PATH" dump xmltree "$APK_PATH" AndroidManifest.xml 2>/dev/null | grep -A1 "package=" | grep "A:" | sed -E "s/.*A: package=\"([^\"]+)\".*/\1/" | head -1)
else
log_error "Neither aapt nor aapt2 found in $ANDROID_HOME/build-tools"
echo "Please ensure your Android build-tools are installed correctly."
exit 1
fi
fi
if [ -n "$ACTUAL_PACKAGE" ]; then
log_success "Determined APK package name: $ACTUAL_PACKAGE"
else
log_warning "Could not determine package name from APK, assuming default: com.proofofpassportapp"
ACTUAL_PACKAGE="com.proofofpassportapp"
fi
# Install the app, replacing any existing installation.
# The -r flag allows adb to replace an existing app, which is safer than
# trying to uninstall first, especially in a CI environment where the
# emulator state might be inconsistent.
echo "Installing app..."
if ! "$ADB_CMD" -s "$EMULATOR_ID" install -r "$APK_PATH"; then
log_error "Android app installation failed"
exit 1
fi
log_success "App successfully installed"
# Verify installation
log_info "🔍 Verifying app installation..."
# Give a moment for installation to settle
sleep 2
# Check if the package is installed using the detected package name
echo "Checking installed packages for: $ACTUAL_PACKAGE"
PACKAGE_CHECK=$("$ADB_CMD" -s "$EMULATOR_ID" shell pm list packages | grep "$ACTUAL_PACKAGE" || echo "")
if [ -n "$PACKAGE_CHECK" ]; then
log_success "App package verified on device: $PACKAGE_CHECK"
else
log_warning "Package '$ACTUAL_PACKAGE' not found, doing broader search..."
# Try searching for parts of the package name
PARTIAL_CHECKS=(
"proofofpassport"
"warroom"
"passport"
)
FOUND_PACKAGE=""
for PARTIAL in "${PARTIAL_CHECKS[@]}"; do
PARTIAL_RESULT=$("$ADB_CMD" -s "$EMULATOR_ID" shell pm list packages | grep "$PARTIAL" || echo "")
if [ -n "$PARTIAL_RESULT" ]; then
echo "Found packages containing '$PARTIAL': $PARTIAL_RESULT"
FOUND_PACKAGE="true"
fi
done
if [ -z "$FOUND_PACKAGE" ]; then
log_error "No related packages found on device"
echo "Attempting to continue anyway - Maestro might still work..."
fi
fi
# Test if the app can be launched directly
log_info "🚀 Testing app launch capability..."
"$ADB_CMD" -s "$EMULATOR_ID" shell am start -n "$ACTUAL_PACKAGE/.MainActivity" || {
log_warning "Direct app launch test failed - this might be expected if the main activity name is different"
}
}
# Cleanup function for Android emulator
cleanup_android_emulator() {
if [ -n "$EMULATOR_PID" ] && kill -0 "$EMULATOR_PID" 2>/dev/null; then
log_info "Cleaning up Android emulator (PID: $EMULATOR_PID)..."
# Kill the emulator process silently
kill "$EMULATOR_PID" >/dev/null 2>&1
# Wait a moment for graceful shutdown
sleep 2
# Force kill if still running
if kill -0 "$EMULATOR_PID" 2>/dev/null; then
kill -9 "$EMULATOR_PID" >/dev/null 2>&1
fi
fi
# Also silence any remaining emulator processes that might be hanging
if [ -n "$FIRST_AVD" ]; then
pkill -f "emulator.*$FIRST_AVD" >/dev/null 2>&1 || true
fi
}
# Main platform runners
run_ios_tests() {
echo "🍎 Starting local iOS e2e testing..."
shutdown_all_simulators
check_metro_running
setup_ios_environment
setup_ios_simulator
build_ios_app
install_ios_app
log_info "⏰ Giving the simulator a moment to settle before starting tests..."
sleep 15
run_maestro_tests
MAESTRO_STATUS=$?
log_success "Local iOS e2e testing completed!"
shutdown_all_simulators
exit $MAESTRO_STATUS
}
run_android_tests() {
echo "🤖 Starting local Android e2e testing..."
# Set up trap to cleanup emulator on script exit
trap cleanup_android_emulator EXIT
# Only check Metro if not in workflow match mode
if [ "$WORKFLOW_MATCH" != "true" ]; then
check_metro_running
fi
setup_android_environment
build_android_app
install_android_app
log_info "⏰ Giving the emulator a moment to settle before starting tests..."
sleep 45
run_maestro_tests
MAESTRO_STATUS=$?
log_success "Local Android e2e testing completed!"
exit $MAESTRO_STATUS
}
# Main execution
main() {
check_directory
if [ -z "$PLATFORM" ]; then
print_usage
exit 1
fi
# Check for workflow match mode
WORKFLOW_MATCH="false"
for arg in "$@"; do
if [ "$arg" = "--workflow-match" ]; then
WORKFLOW_MATCH="true"
log_info "🔧 Running in workflow match mode (Release builds, no Metro)"
break
fi
done
setup_maestro
build_dependencies
case "$PLATFORM" in
ios)
run_ios_tests
;;
android)
run_android_tests
;;
*)
log_error "Invalid platform: $PLATFORM"
echo "Valid options: ios, android"
exit 1
;;
esac
}
# Run main function
main "$@"

View File

@@ -1,11 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
// https://docs.ethers.org/v6/cookbook/react-native/
// eslint-disable-next-line simple-import-sort/imports
import { ethers } from 'ethers';
import { hmac } from '@noble/hashes/hmac';
import { pbkdf2 as noblePbkdf2 } from '@noble/hashes/pbkdf2';
import { sha256 as nobleSha256 } from '@noble/hashes/sha256';
import { sha512 as nobleSha512 } from '@noble/hashes/sha512';
import { ethers } from 'ethers';
function randomBytes(length: number): Uint8Array {
if (typeof globalThis.crypto?.getRandomValues !== 'function') {

View File

@@ -0,0 +1,6 @@
appId: com.proofofpassportapp
---
- launchApp
- extendedWaitUntil:
visible: "I have a Passport or Biometric ID"
timeout: 30000

View File

@@ -0,0 +1,6 @@
appId: com.warroom.proofofpassport
---
- launchApp
- extendedWaitUntil:
visible: "I have a Passport or Biometric ID"
timeout: 30000

View File

@@ -1,10 +1,11 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
// Register crypto polyfills
import '../../src/utils/ethers';
// eslint-disable-next-line simple-import-sort/imports
import { ethers } from 'ethers';
import '../../src/utils/ethers';
describe('ethers crypto polyfills', () => {
it('randomBytes returns requested length and unique values', () => {
const a = ethers.randomBytes(16);

View File

@@ -10,7 +10,9 @@
],
"scripts": {
"build": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run build",
"format": "yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
"format": "yarn format:root && yarn format:github && yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
"format:github": "prettier --parser yaml --write .github/**/*.yml --single-quote false",
"format:root": "prettier --parser markdown --write *.md && prettier --parser yaml --write .*.{yml,yaml} --single-quote false",
"gitleaks": "gitleaks protect --staged --redact --config=.gitleaks.toml",
"postinstall": "patch-package",
"lint": "yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint",