Skip to content

[FEATURE] Support launchApp: arguments on Real iOS Devices #7

@majdukovic

Description

@majdukovic

Problem Statement

The launchApp: arguments feature works correctly on iOS Simulators but silently fails on real iOS devices. This blocks critical E2E testing patterns that require passing data (particularly authentication tokens) to the app at launch time.

Current behavior:

  • On iOS Simulators: Arguments passed via launchApp: arguments are accessible in the app via ProcessInfo.processInfo.arguments
  • On real iOS devices: Arguments are silently ignored - the app does not receive them via ProcessInfo.processInfo.arguments

Root cause: iOS Simulators support arguments via xcrun simctl launch, but real devices use WebDriverAgent (WDA) with XCUITest's launch mechanism, which doesn't propagate arguments to the target app's process in the same way.

This limitation forces tests requiring authenticated users to navigate full sign-in UI flows on real devices, making tests slower, flakier, and dependent on external auth providers.

Proposed Solution

Option A (Preferred): Make launchApp: arguments work consistently across simulators and real devices. Arguments should be accessible via ProcessInfo.processInfo.arguments on both platforms.

Option B: If Option A is not technically feasible due to iOS/WDA limitations, provide an alternative mechanism:

  • Support setting UserDefaults values before app launch
  • Add a launchApp: environment option for environment variables
  • Provide clear documentation of the limitation with recommended workarounds

Option C (Minimum): Emit a warning when launchApp: arguments is used on real iOS devices (non-simulator), alerting users that arguments may not work as expected.

Use Case

We uses maestro-runner for E2E UI testing with the following pattern:

  1. Create test user: Use runScript to call an E2E auth API and generate a test user with an auth token
  2. Launch app with token: Pass the auth token via launchApp: arguments so the app can auto-authenticate
  3. Run test flows: Execute UI tests (onboarding, features, etc.) without manually navigating sign-in screens

This pattern is essential for:

  • Speed: Bypassing login UI saves 10-30 seconds per test
  • Reliability: Avoids dependency on external auth providers (Google OAuth, email delivery)
  • Isolation: Each test gets a fresh, authenticated user without state pollution
  • Real device testing: Required for catching device-specific bugs (performance, networking, push notifications, biometrics)

App-side implementation (Swift):

// In didFinishLaunchingWithOptions:
if isE2EMode {
    var token: String?
    // Check process arguments (Appium/XCUITest style)
    let arguments = ProcessInfo.processInfo.arguments
    if let tokenIndex = arguments.firstIndex(of: "-E2E_CUSTOM_AUTH_TOKEN"),
       arguments.count > tokenIndex + 1 {
        token = arguments[tokenIndex + 1]
    }
    // Fallback to UserDefaults (Maestro style)
    if token == nil {
        token = UserDefaults.standard.string(forKey: "E2E_CUSTOM_AUTH_TOKEN")
    }
    if let token = token, !token.isEmpty {
        appSetup.signInWithE2EToken(token: token)
    }
}

Example

appId: ai.stormlabs.treework.dev
env:
  E2E_API_URL: https://our-e2e-auth-service.run.app/api/v1/e2e-auth
  E2E_API_KEY: <api-key>
---
# Step 1: Create test user via API and get auth token
- runScript: ../../scripts/create-test-user.js

# Step 2: Launch app with auth token passed as argument
- launchApp:
    appId: ai.stormlabs.treework.dev
    clearState: true
    arguments:
      E2E_CUSTOM_AUTH_TOKEN: ${output.testUser.token}

# Step 3: App auto-authenticates and shows onboarding (authenticated state)
- assertVisible: "Your privacy is our priority."

# Step 4: Continue with test flow
- tapOn: "Continue"
# ... rest of test

Expected behavior: The app receives E2E_CUSTOM_AUTH_TOKEN via ProcessInfo.processInfo.arguments and auto-authenticates the user, skipping the login screen.

Actual behavior on real device: Arguments are silently ignored, app shows login screen, test fails at assertVisible step.

Alternatives Considered

  1. Deep links: Could pass token via URL scheme (e.g., myapp://e2e-auth?token=xyz)

    • Pros: Works on real devices
    • Cons: Requires app code changes, less elegant, URL length limits, security concerns with tokens in URLs
  2. Manual login flow in tests: Navigate sign-in UI in every test

    • Pros: No workarounds needed
    • Cons: Slow (adds 10-30s per test), flaky (depends on external services), harder to maintain
  3. Pre-install app with baked-in credentials: Build test-specific IPA with hardcoded tokens

    • Pros: No runtime argument passing needed
    • Cons: Security risk, requires separate builds, tokens expire, not scalable
  4. Use only simulators: Skip real device testing entirely

    • Pros: launchApp: arguments works perfectly
    • Cons: Misses device-specific bugs, not acceptable for production testing

None of these alternatives are as clean or reliable as fixing launchApp: arguments on real devices.

Additional Context

Environment:

  • maestro-runner: v1.0.4 (Go binary, WDA driver)
  • Real device: iPhone (iOS 18.2.1, UDID: 00008030-0001353934D3402E)
  • WDA: v11.1.4 (auto-downloaded by maestro-runner)
  • macOS: Apple Silicon
  • App: Kotlin Multiplatform (Compose Multiplatform) with SwiftUI shell

Command used:

maestro-runner \
  --platform ios \
  --device 00008030-0001353934D3402E \
  --team-id Z5Q3WUX123 \
  --app-file "path/to/app.ipa" \
  --verbose \
  test flows/onboarding/onboarding-user-profile-validation.yaml

Evidence from test run:

✓ runScript (993ms)              ← Test user created, token generated
  launchApp (clearState) (20.5s) ← App launched but arguments NOT passed
✗ assertVisible (17.3s)          ← Failed: user not authenticated
  ╰─ Element not visible: text='Your privacy is our priority.'

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions