Files
honeyDueKMP/iosApp/iosApp/Helpers/UITestRuntime.swift
T
Trey T d11cc82fec Perf: inject auth token at launch to skip the UI login (~26-50% faster)
Measured: ~half of every authenticated test was fixed setup, dominated by the
UI login (typing email+password, keyboard/SecureField dance, ~8-12s). The test
already creates the account via API and holds its real Kratos session token —
so instead of typing credentials, pass the token as a launch arg and boot the
app already authenticated.

- App (UITestRuntime + iOSApp): reads --ui-test-session-token; after the
  --reset-state clear, calls DataManager.setAuthToken(token) and replicates the
  post-login init the UI login path runs (getCurrentUser + initializeLookups +
  getMyResidences + getTasks) so owner-gated/data-gated screens (residence
  detail delete + manage-users, pickers, lists) work on boot. Guarded by
  UITestRuntime.isEnabled — no effect on production.
- AuthenticatedUITestCase: in fresh-account mode, create the account + seed its
  preconditions BEFORE launch, expose the token via additionalLaunchArguments,
  and drop the UI login. Legacy (usesFreshAccount=false) suites still UI-login.

Measured per-test medians: Contractor 34s -> 25s; Task (uses lookups) ~34s ->
16s. TESTING.md updated. All affected suites pass; 0 leaked accounts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:27:39 -05:00

90 lines
3.2 KiB
Swift

import Foundation
import UIKit
import ComposeApp
/// Runtime contract between the app and XCUITests.
enum UITestRuntime {
// i18n-ignore-begin: launch-argument flag identifiers for XCUITests (non-UI)
static let uiTestingFlag = "--ui-testing"
static let disableAnimationsFlag = "--disable-animations"
static let resetStateFlag = "--reset-state"
static let mockAuthFlag = "--ui-test-mock-auth"
static let completeOnboardingFlag = "--complete-onboarding"
static let sessionTokenFlag = "--ui-test-session-token"
// i18n-ignore-end
static var launchArguments: [String] {
ProcessInfo.processInfo.arguments
}
static var isEnabled: Bool {
launchArguments.contains(uiTestingFlag)
}
static var shouldDisableAnimations: Bool {
isEnabled && launchArguments.contains(disableAnimationsFlag)
}
static var shouldResetState: Bool {
isEnabled && launchArguments.contains(resetStateFlag)
}
static var shouldMockAuth: Bool {
isEnabled && launchArguments.contains(mockAuthFlag)
}
static var shouldCompleteOnboarding: Bool {
isEnabled && launchArguments.contains(completeOnboardingFlag)
}
/// A real Kratos session token supplied by an authenticated UI test so the
/// app can boot already logged in (skipping the slow/flaky UI login). The
/// test obtains this token when it creates the account via API.
static var injectedSessionToken: String? {
guard isEnabled else { return nil }
let args = launchArguments
guard let i = args.firstIndex(of: sessionTokenFlag), i + 1 < args.count else { return nil }
let token = args[i + 1]
return token.isEmpty ? nil : token
}
static func configureForLaunch() {
guard isEnabled else { return }
if shouldDisableAnimations {
UIView.setAnimationsEnabled(false)
}
UserDefaults.standard.set(true, forKey: "ui_testing_mode")
// Mark onboarding complete synchronously before SwiftUI renders,
// so RootView routes to the standalone LoginView instead of OnboardingCoordinator.
if shouldCompleteOnboarding {
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
}
}
@MainActor static func resetStateIfRequested() {
guard shouldResetState else { return }
DataManager.shared.clear()
OnboardingState.shared.reset()
ThemeManager.shared.currentTheme = .bright
UserDefaults.standard.removeObject(forKey: "ui_test_user_verified")
// Re-apply onboarding completion after reset. Set the flag directly
// because completeOnboarding() has an auth guard that fails here
// (DataManager was just cleared, so isAuthenticated is false).
if shouldCompleteOnboarding {
OnboardingState.shared.hasCompletedOnboarding = true
}
}
/// Mark onboarding as complete so the app shows the standalone login
/// instead of the onboarding coordinator. Called after resetState (if any).
@MainActor static func completeOnboardingIfRequested() {
guard shouldCompleteOnboarding else { return }
OnboardingState.shared.completeOnboarding()
}
}